Connect to new BMW CarData service through MQTT

Hi there,

based on the discussion in the topic of the now deprecated MyBMW binding, i created a solution to receive data through the new MQTT datastream.

0. BMW MQTT Integration

Prerequisites:

  • BMW Connected Drive account
  • openHAB 5.0.1
  • MQTT binding
  • Telegram binding (optional)

There are several steps to get this running and you need to ensure all prerequisites are met.
Please read through every step before starting the process.

  1. Create a Client_ID and get your tokens
    1. Manual setup in the BMW portals
    2. Setup with openHAB and Telegram
    3. Setup with openHAB and logviewer
  2. Create a MQTT Broker and Generic thing
    2.1. Create channels in the Genering thing for your items
  3. Create the JSRules to refresh the tokens and update your Broker thing
  4. Create more channels :slight_smile:

Methods 1.2 and 1.3 use items to store your tokens and start the process.

1.1 BMW Portal

Please read ALL steps before you start and make sure you do NOT close any browser tabs/windows in the process.

  1. Open the vehicle overview and select “BMW CarData”
    https://www.bmw.de/de-de/mybmw/vehicle-overview

  2. Under “Technical access to BMW CarData”, select “Create CarData Client”
    Copy the Client_ID and enable both switches — wait a moment before activating the second one.
    If you receive an error message, close the tab and reload the page.

  3. Open the “CarData Customer Portal” and select “Device Code Flow API”
    CarData Customer Portal

  4. Expand “Device Flow with PKCE” and then expand “gcdm/oauth/device/code” (Starts the device code flow)

  5. Click “Try it out”, insert the Client_ID from step 2, and click Execute.

  6. Further down, the response with code 200 should appear under Responses.
    Here you will find two additional pieces of information: the User_Code and the Device_Code — copy both again.
    These values are required for authorization and for requesting the MQTT password.

  7. In the tab from step 2, click “Authenticate device”, log in with your ConnectedDrive account, and enter the User_Code from step 6 in the new tab.
    The entry should be confirmed with “Device authorized”.

  8. Go back to the BMW CarData page, scroll down to “CarData Streaming”, and create a stream.

  9. Expand the connection details and copy the username (gcid).

  10. Under “Change data selection”, you can now select all the required data.
    Note: There are over 200 data points, though not all are supported by every vehicle.
    If you like, you can automate this via the browser console — see the script at the end of this section.

  11. Go back to the “CarData Customer Portal” and expand “/gcdm/oauth/token” (Request a token for the device).

  12. Click “Try it out”, insert your Client_ID and Device_Code, and confirm by clicking Execute.
    You should again see a response with Response Code 200.

  13. The response now contains several IDs and tokens:

  • gcid = username, identical to step 9
  • access_token = used to access the CarData API
  • refresh_token = token used to renew all tokens
  • id_token = the password for MQTT

Finally, you can test the data using the MQTTX Client.
Here’s a quick guide:

For the Subscriber, use your gcid/VIN, e.g.
12345678-1234-1234-123456789012/WBA1AB23456CD78901

Then, trigger an update via the MyBMW app, for example by locking the vehicle.
You should then receive the data packets.


Script to automate the activation of all data points.
Note: It takes some time after you hit enter and the tab may get unresponsive, just wait.

(() => {
  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.`);
})();

Script from GitHub - JjyKsi/bmw-cardata-ha

How to enter the Client_ID into carBMW_Client_ID

As there´s some confusion around entering your Client_ID into the item carBMW_Client_ID, here´s a quick guide.
You can change the state of an item throug the main UI API Explorer.
Just open http://:8080/developer/api-explorer, open items and search for PUT - /items/{itemname}/state and open it.
Click on Try it out in the top right corner.
Enter the itemname carBMW_Client_ID as itemname and your Client_ID as Valid item state where you override the example ON.
Pres Execute and this will set the state of carBMW_Client_ID to your Client_ID.

1.2 openHAB Telegram

1.3 openHAB logviewer

This rule allows you to automatically do the steps 3 to 13 of the manual setup.
You can choose between two modes with the global variable MessageMode in line 8 of the rule.
The rule will store the expire time of your tokens into the same item that is used for the refresh.

1.2 telegram

You need the Telegram binding installed and configured.
You need to configure three variables (line 22 to 25) with the data from your Telegram binding: Your bot ID, the item that stores lastMessageText and the item that stores replyId
To start the setup, you just input your Client_ID into the item carBMW_Client_ID as this will trigger the rule.
After that you´ll receive two telegram messages, one with the link to authorize your device code and one to confirm you did.
Click the link, use your BMW credentials to authorize your Client_ID and click Yes afterwards.
Now the rules should get your tokens and store them into the items.

1.3 logviewer

Please open the log viewer through http://<openHAB>:8080/developer/log-viewer and enter the filter BMW_Token.
Then you need to input your Client_ID into the item carBMW_Client_ID as this will trigger the setup.
You should see multiple log entries starting with the entry BMW Token Setup - Step 1.
One of the log messages shows the link to authorize your Client_ID, click on the log entry, copy the link into a new tab and authorize with your BMW credentials.
The rule will wait 30 seconds for you to authorize and then will start trying to get your tokens.
It will do 10 retries with 6 seconds in between.
Once the tokens were successfully received, you should see entries with your new tokens and BMW Token Setup Complete!.

Telegram and Logviewer

If you also installed the BMW_ID_Token_Updates.js your MQTT broker thing will be updated with the new ID Token.
Please note that you need to have the MQTT broker thing for this to work.

Items

String carBMW_Token_Access "Access Token [%s]" (gPersist, gBMWTokens)
String carBMW_Token_Refresh "Refresh Token [%s]" (gPersist, gBMWTokens)
String carBMW_Token_ID "ID Token [%s]" (gPersist, gBMWTokens)
String carBMW_Client_ID "Client ID [%s]" (gPersist, gBMWTokens)
String carBMW_Username "Username [%s]" (gPersist, gBMWTokens)
String carBMW_Code_Device "Device Code [%s]" (gPersist, gBMWTokens)
String carBMW_Code_User "User Code [%s]" (gPersist, gBMWTokens)
String carBMW_Code_Challenge "Challenge Code [%S]" (gPersist, gBMWTokens)
String carBMW_Code_Verifier "Verifier Code [%S]" (gPersist, gBMWTokens)
Switch carBMW_Token_Refresh_Trigger "Manual Token update" (gBMWTokens)
Switch carBMW_Token_Request "Start Token Request" (gBMWTokens)
Number carBMW_Expire "Expiry date" (gPersist, gBMWTokens)

BMW_Token_Setup.js

/**
 * BMW Token Initial Setup Rule
 * Creates initial tokens using Device Code Flow
 */

// Global configuration
var TELEGRAM_NOTIFY_ERROR = true; // Send Telegram notification on errors
var MessageMode = 'telegram'; // Set this mode to telegram or logviewer

// Item configuration
var refreshTokenExpiryItem = 'carBMW_Expire';
var clientIDItem = 'carBMW_Client_ID';
var usernameItem = 'carBMW_Username';
var codeVerifierItem = 'carBMW_Code_Verifier';
var codeChallengeItem = 'carBMW_Code_Challenge';
var deviceCodeItem = 'carBMW_Code_Device';
var userCodeItem = 'carBMW_Code_User';
var tokenRequestSwitch = 'carBMW_Token_Request';
var tokenAccessItem = 'carBMW_Token_Access';
var tokenRefreshItem = 'carBMW_Token_Refresh';
var tokenIdItem = 'carBMW_Token_ID';

// Telegram configuration
// Ignore if you set MessageMode to logviewer
var bot1 = 134757258; // Michael
var tgAddOnId = 'telegram:telegramBot:bot'; // Enter your telegram binding ID
var tgReplyItem = 'tgReplyId'; // Item that receives the reply ID
var tgLastMessageItem = 'tgLastMessageText'; // Item that receives the last message text

//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
// please don't change the code from here
//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
var telegramAction = actions.get('telegram', tgAddOnId);
var refreshTokenExpiry = items.getItem(refreshTokenExpiryItem);

// Function to escape special characters for Telegram
function escapeTelegram(text) {
    if (text === null || text === undefined) {
        return "";
    }
    text = text.toString(); // Sicherheit für Number/Boolean-Werte
    if (text.includes('{') || text.includes('[')) {
        // Setze in Code-Block (dann kein Escaping nötig)
        return '`' + text + '`';
    }
    // Sonst normale Zeichen escapen
    return text.replace(/[_*`[\]]/g, '\\$&');
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to send messages based on MessageMode
function sendMessage(logger, message, isError) {
    switch (MessageMode) {
        case 'telegram':
            if (telegramAction !== null && (isError ? TELEGRAM_NOTIFY_ERROR : true)) {
                telegramAction.sendTelegram(bot1, message);
            }
            break;
        case 'logviewer':
            if (isError) {
                logger.error(message.replace(/[🔐✅❌⚠️]/g, '').trim());
            } else {
                logger.info(message.replace(/[🔐✅❌⚠️]/g, '').trim());
            }
            break;
        default:
            logger.warn("Unknown MessageMode: " + MessageMode);
    }
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to generate random code_verifier (32 random bytes as Base64url)
function generateCodeVerifier() {
    var SecureRandom = Java.type("java.security.SecureRandom");
    var Base64 = Java.type("java.util.Base64");
    
    var random = new SecureRandom();
    var bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 32);
    random.nextBytes(bytes);
    
    // Base64url encoding (without padding)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to generate code_challenge from code_verifier (SHA-256 + Base64url)
function generateCodeChallenge(verifier) {
    var MessageDigest = Java.type("java.security.MessageDigest");
    var Base64 = Java.type("java.util.Base64");
    var StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
    
    var digest = MessageDigest.getInstance("SHA-256");
    var hash = digest.digest(verifier.getBytes(StandardCharsets.UTF_8));
    
    // Base64url encoding (without padding)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to enode the header parameters for sendHttpPostRequest
function encodeParam(key, value) {
    return java.net.URLEncoder.encode(key, "UTF-8") + "=" + java.net.URLEncoder.encode(value, "UTF-8");
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to convert EPOCH to DateTime
function formatDate(epochMillis) {
    var date = new Date(epochMillis);

    var day = String(date.getDate()).padStart(2, '0');
    var month = String(date.getMonth() + 1).padStart(2, '0');
    var year = date.getFullYear();
    var hours = String(date.getHours()).padStart(2, '0');
    var minutes = String(date.getMinutes()).padStart(2, '0');
    var seconds = String(date.getSeconds()).padStart(2, '0');

    return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
}

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Step 1: Start Device Code Flow
rules.JSRule({
    name: "BMW Token Setup - Step 1: Device Code Request",
    description: "Requests device code and user code when Client ID changes",
    triggers: [
        triggers.ItemStateUpdateTrigger(clientIDItem)
    ],
    execute: function(event) {
        var logger = log("BMW_Token_Setup_Step1");
                
        try {
            var clientId = items.getItem(clientIDItem).state.toString();
            
            // Check if Client ID is valid
            if (clientId === "NULL" || clientId === "UNDEF" || clientId === "") {
                logger.warn("Client ID is empty. Cannot start Device Code Flow.");
                return;
            }
            
            logger.debug("Starting BMW Device Code Flow for Client ID: " + clientId);
            
            // Generate code_verifier and code_challenge
            var codeVerifier = generateCodeVerifier();
            var codeChallenge = generateCodeChallenge(codeVerifier);
            
            // Store verifier and challenge in items
            items.getItem(codeVerifierItem).postUpdate(codeVerifier);
            items.getItem(codeChallengeItem).postUpdate(codeChallenge);
            
            logger.debug("Generated code_verifier and code_challenge");
            logger.debug("Code Verifier: " + codeVerifier);
            logger.debug("Code Challenge: " + codeChallenge);
            
            // Prepare HTTP POST request with URL encoding
            var url = "https://customer.bmwgroup.com/gcdm/oauth/device/code";
            
            // URL encode the scope parameter (spaces must be encoded as %20 or +)
            var scopeValue = "authenticate_user openid cardata:streaming:read cardata:api:read";

            // Build the encoded payload
            var payload = [
                encodeParam("client_id", clientId),
                encodeParam("response_type", "device_code"),
                encodeParam("scope", scopeValue),
                encodeParam("code_challenge", codeChallenge),
                encodeParam("code_challenge_method", "S256")
            ].join("&");
            
            // Create headers as Java HashMap
            var HashMap = Java.type("java.util.HashMap");
            var headers = new HashMap();
            headers.put("Accept", "application/json");
                      
            // Execute HTTP request
            var response = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, 10000);
            
            // Check if response is valid
            if (response === null || response === undefined || response === "") {
                logger.error("No valid response received from BMW.");
                sendMessage(logger, "❌ BMW Token Setup Failed\n\nNo valid response received from BMW during Device Code request.", true);
                return;
            }
            
            logger.debug("Received response: " + response);
            
            // Parse JSON response
            var jsonResponse = JSON.parse(response);
            
            // Check for errors
            if (jsonResponse.error) {
                var errorMsg = jsonResponse.error_description || jsonResponse.error;
                var errorCode = jsonResponse.error;
                
                logger.error("BMW API Error Code: " + errorCode);
                logger.error("BMW API Error Description: " + errorMsg);
                logger.error("Full JSON Response: " + response);
                
                sendMessage(logger, "❌ BMW Token Setup Failed\n\nBMW API Error during Device Code request\n\nError Code: " + 
                    escapeTelegram(errorCode) + "\nDescription: " + escapeTelegram(errorMsg), true);
                return;
            }
            
            // Check if all required fields are present
            if (!jsonResponse.user_code || !jsonResponse.device_code || !jsonResponse.verification_uri_complete) {
                logger.error("Incomplete response received. Missing required fields.");
                logger.error("Response: " + response);
                
                sendMessage(logger, "❌ BMW Token Setup Failed\n\nIncomplete response received from BMW.\n\nResponse: " + 
                    escapeTelegram(response), true);
                return;
            }
            
            // Store device code and user code in items
            items.getItem(deviceCodeItem).postUpdate(jsonResponse.device_code);
            items.getItem(userCodeItem).postUpdate(jsonResponse.user_code);
            
            logger.debug("Device Code and User Code successfully received and stored.");
            logger.debug("User Code: " + jsonResponse.user_code);
            logger.debug("Verification URI: " + jsonResponse.verification_uri_complete);
            
            // Handle different MessageModes
            switch (MessageMode) {
                case 'telegram':
                    // Send verification URI via Telegram
                    if (telegramAction !== null) {
                        telegramAction.sendTelegram(bot1, "🔐 BMW Token Setup - Step 1\n\n" +
                            "Please verify your Client ID by opening this link:\n\n" +
                            escapeTelegram(jsonResponse.verification_uri_complete) + "\n\n" +
                            "User Code: " + escapeTelegram(jsonResponse.user_code) + "\n\n" +
                            "This link expires in " + jsonResponse.expires_in + " seconds (" + Math.floor(jsonResponse.expires_in / 60) + " minutes).");
                        
                        // Wait a moment before sending the question
                        java.lang.Thread.sleep(2000);
                        
                        // Send question via Telegram
                        telegramAction.sendTelegramQuery(bot1, 
                            "Have you completed the verification?\n\nShould I request the tokens now?",
                            "BMW_Token_Request",
                            "Yes", "No");
                    }
                    break;
                    
                case 'logviewer':
                    // Log verification info
                    logger.info("BMW Token Setup - Step 1");
                    logger.info("Please verify your Client ID by opening this link:");
                    logger.info(jsonResponse.verification_uri_complete);
                    logger.info("User Code: " + jsonResponse.user_code);
                    logger.info("This link expires in " + jsonResponse.expires_in + " seconds (" + Math.floor(jsonResponse.expires_in / 60) + " minutes).");
                    logger.info("Automatically triggering token request...");
                    
                    // Automatically trigger token request for logviewer mode
                    items.getItem(tokenRequestSwitch).postUpdate("ON");
                    break;
                    
                default:
                    logger.warn("Unknown MessageMode: " + MessageMode);
            }
            
        } catch (error) {
            logger.error("Exception during Device Code request: " + error);
            logger.error("Error type: " + error.name);
            logger.error("Error message: " + error.message);
            logger.error("Stack trace: " + error.stack);
            
            sendMessage(logger, "❌ BMW Token Setup Exception\n\nException occurred during Device Code request.\n\nError: " + 
                escapeTelegram(error.toString()) + "\nType: " + escapeTelegram(error.name) + "\nMessage: " + 
                escapeTelegram(error.message), true);
        }
    }
});

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Step 1.5: Handle Telegram reply (only for telegram mode)
rules.JSRule({
    name: "BMW Token Setup - Step 1.5: Handle Telegram Reply",
    description: "Processes the Telegram reply about verification completion",
    triggers: [
        triggers.ItemStateChangeTrigger(tgReplyItem, undefined, "BMW_Token_Request")
    ],
    execute: function(event) {
        var logger = log("BMW_Token_Setup_Reply");
        
        // Skip this step in logviewer mode
        if (MessageMode !== 'telegram') {
            logger.debug("MessageMode is not 'telegram', skipping Telegram reply handling.");
            return;
        }
        
        var replyId = items.getItem(tgReplyItem).state.toString();
        var replyText = items.getItem(tgLastMessageItem).state.toString();
        
        try {
            logger.debug("Received Telegram reply: " + replyText);
            
            if (replyId === "BMW_Token_Request") {
                if (replyText === "Yes") {
                    logger.debug("User confirmed verification. Setting " + tokenRequestSwitch.toString() + " to ON.");
                    
                    // Set item to trigger token request
                    items.getItem(tokenRequestSwitch).postUpdate("ON");
                    
                    // Send acknowledgement
                    if (telegramAction !== null) {
                        telegramAction.sendTelegramAnswer(bot1, "BMW_Token_Request", "✅ Starting token request...");
                    }
                } else if (replyText === "No") {
                    logger.debug("User declined. Token request cancelled.");
                    
                    // Send acknowledgement
                    if (telegramAction !== null) {
                        telegramAction.sendTelegramAnswer(bot1, "BMW_Token_Request", "❌ Token request cancelled.\n\n" +
                            "Please complete the verification and trigger the setup again by updating " + clientIDItem + ".");
                    }
                }
            }
            
        } catch (error) {
            logger.error("Exception processing Telegram reply: " + error);
            logger.error("Stack trace: " + error.stack);
        }
    }
});

//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Step 2: Request tokens
rules.JSRule({
    name: "BMW Token Setup - Step 2: Token Request",
    description: "Requests access tokens after device code verification",
    triggers: [
        triggers.ItemStateUpdateTrigger(tokenRequestSwitch, "ON")
    ],
    execute: function(event) {
        var logger = log("BMW_Token_Setup_Step2");
              
        try {
            var clientId = items.getItem(clientIDItem).state.toString();
            var deviceCode = items.getItem(deviceCodeItem).state.toString();
            var codeVerifier = items.getItem(codeVerifierItem).state.toString();
            
            // Check if all required values are present
            if (clientId === "NULL" || clientId === "UNDEF" || clientId === "" ||
                deviceCode === "NULL" || deviceCode === "UNDEF" || deviceCode === "" ||
                codeVerifier === "NULL" || codeVerifier === "UNDEF" || codeVerifier === "") {
                logger.error("Required values are missing. Cannot request tokens.");
                
                sendMessage(logger, "❌ BMW Token Request Failed\n\nRequired values (Client ID, Device Code, or Code Verifier) are missing.\n\n" +
                    "Please restart the setup process.", true);
                
                items.getItem(tokenRequestSwitch).postUpdate("OFF");
                return;
            }
            
            // Handle different MessageModes for waiting and retry logic
            var maxRetries = 0;
            var initialWait = 0;
            var retryDelay = 0;
            
            switch (MessageMode) {
                case 'telegram':
                    // No waiting, no retries for telegram mode
                    maxRetries = 0;
                    initialWait = 0;
                    retryDelay = 0;
                    logger.debug("Requesting tokens from BMW (Telegram mode)...");
                    break;
                    
                case 'logviewer':
                    // Wait 30 seconds initially, then retry up to 10 times with 6 second delay
                    maxRetries = 10;
                    initialWait = 30000; // 30 seconds
                    retryDelay = 6000; // 6 seconds
                    logger.info("Waiting 30 seconds before requesting tokens (Logviewer mode)...");
                    java.lang.Thread.sleep(initialWait);
                    logger.info("Requesting tokens from BMW...");
                    break;
                    
                default:
                    logger.warn("Unknown MessageMode: " + MessageMode);
                    maxRetries = 0;
                    initialWait = 0;
                    retryDelay = 0;
            }
            
            // Prepare HTTP POST request
            var url = "https://customer.bmwgroup.com/gcdm/oauth/token";

            // Build the encoded payload
            var payload = [
                encodeParam("client_id", clientId),
                encodeParam("device_code", deviceCode),
                encodeParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
                encodeParam("code_verifier", codeVerifier),
            ].join("&");
            
            // Create headers as Java HashMap
            var HashMap = Java.type("java.util.HashMap");
            var headers = new HashMap();

            // Retry loop for logviewer mode
            var attempt = 0;
            var success = false;
            var jsonResponse = null;
            
            while (attempt <= maxRetries && !success) {
                if (attempt > 0) {
                    logger.info("Retry attempt " + attempt + " of " + maxRetries + " after " + (retryDelay/1000) + " seconds...");
                    java.lang.Thread.sleep(retryDelay);
                }
                
                // Execute HTTP request
                var response = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, 10000);
                
                // Check if response is valid
                if (response === null || response === undefined || response === "") {
                    logger.error("No valid response received from BMW.");
                    
                    sendMessage(logger, "❌ BMW Token Request Failed\n\nNo valid response received from BMW during token request.", true);
                    
                    items.getItem(tokenRequestSwitch).postUpdate("OFF");
                    return;
                }
                
                logger.debug("Received response: " + response);
                
                // Parse JSON response
                jsonResponse = JSON.parse(response);
                
                // Check for errors
                if (jsonResponse.error) {
                    var errorMsg = jsonResponse.error_description || jsonResponse.error;
                    var errorCode = jsonResponse.error;
                    
                    logger.error("BMW API Error Code: " + errorCode);
                    logger.error("BMW API Error Description: " + errorMsg);
                    
                    // Handle authorization_pending in logviewer mode
                    if (errorCode === "authorization_pending" && MessageMode === 'logviewer' && attempt < maxRetries) {
                        logger.info("Authorization still pending, will retry...");
                        attempt++;
                        continue; // Retry
                    }
                    
                    // For all other errors or if max retries reached, abort
                    logger.error("Full JSON Response: " + response);
                    
                    var errorMessage = "❌ BMW Token Request Failed\n\nBMW API Error\n\nError Code: " + 
                        escapeTelegram(errorCode) + "\nDescription: " + escapeTelegram(errorMsg);
                    
                    // Add specific hints based on error code
                    if (errorCode === "authorization_pending") {
                        errorMessage += "\n\n⚠️ The verification is not yet completed. Please complete the verification at the provided link first.";
                    } else if (errorCode === "invalid_client") {
                        errorMessage += "\n\n⚠️ Client authentication failed. Please check your Client ID.";
                    } else if (errorCode === "invalid_request" || errorCode === "invalid_token") {
                        errorMessage += "\n\n⚠️ The request is invalid or a parameter is missing. Please restart the setup process.";
                    }
                    
                    sendMessage(logger, errorMessage, true);
                    
                    items.getItem(tokenRequestSwitch).postUpdate("OFF");
                    return;
                }
                
                // No error, success!
                success = true;
            }
            
            // Check if we exceeded retries
            if (!success) {
                logger.error("Token request failed after " + maxRetries + " retries.");
                sendMessage(logger, "❌ BMW Token Request Failed\n\nAuthorization still pending after " + maxRetries + 
                    " retries.\n\nPlease complete the verification and try again.", true);
                items.getItem(tokenRequestSwitch).postUpdate("OFF");
                return;
            }
            
            // Check if all required fields are present
            if (!jsonResponse.access_token || !jsonResponse.refresh_token || !jsonResponse.id_token) {
                logger.error("Incomplete token response received.");
                logger.error("Response: " + JSON.stringify(jsonResponse));
                
                sendMessage(logger, "❌ BMW Token Request Failed\n\nIncomplete token response received.\n\nResponse: " + 
                    escapeTelegram(JSON.stringify(jsonResponse)), true);
                
                items.getItem(tokenRequestSwitch).postUpdate("OFF");
                return;
            }
            
            // Get the stored and received username
            var usernameItemState = items.getItem(usernameItem).state
            var usernameStored = (usernameItemState === null || usernameItemState.toString() === "NULL" || usernameItemState.toString() === "UNDEF") ? "" : usernameItemState.toString();
            var usernameJSON = jsonResponse.gcid;

            // Check if username exists in response
            if (!usernameJSON) {
                logger.warn("No username (gcid) found in response.");
            } else if (usernameStored === "NULL" || usernameStored === "UNDEF" || usernameStored === "") {
                logger.info("No username stored yet. Saving username: " + usernameJSON);
                items.getItem(usernameItem).postUpdate(usernameJSON);
            } else if (usernameStored !== usernameJSON) {
                logger.info("Stored username is not identical to received username. Username will be updated!");
                logger.info("Old: " + usernameStored + " -> New: " + usernameJSON);
                items.getItem(usernameItem).postUpdate(usernameJSON);
            } else {
                logger.info("Stored username and received username match.");
            }
            
            // Store tokens in items
            items.getItem(tokenAccessItem).postUpdate(jsonResponse.access_token);
            items.getItem(tokenRefreshItem).postUpdate(jsonResponse.refresh_token);
            items.getItem(tokenIdItem).postUpdate(jsonResponse.id_token);

            // Calculate expiry time of the token minus 5 minutes
            var expireDateTime = Date.now() + (parseInt(jsonResponse.expires_in) * 1000) - 5*60*1000;
            var readableTime = formatDate(expireDateTime);

            // Safe the expiry time into the item
            refreshTokenExpiry.postUpdate(expireDateTime);
            
            logger.debug("Tokens successfully received and stored!");
            logger.debug("Access Token expires in: " + jsonResponse.expires_in + " seconds");
            
            // Send success message
            sendMessage(logger, "✅ BMW Token Setup Complete!\n\nAll tokens have been successfully created and stored.\n\n" +
                "Access Token expires in: " + jsonResponse.expires_in + " seconds (" + readableTime.toString() + ")\n\n" +
                "Automatic token refresh will take over every 55 minutes.", false);
            
            // Reset trigger item
            items.getItem(tokenRequestSwitch).postUpdate("OFF");
            
        } catch (error) {
            logger.error("Exception during token request: " + error);
            logger.error("Error type: " + error.name);
            logger.error("Error message: " + error.message);
            logger.error("Stack trace: " + error.stack);
            
            sendMessage(logger, "❌ BMW Token Request Exception\n\nException occurred during token request.\n\nError: " + 
                escapeTelegram(error.toString()) + "\nType: " + escapeTelegram(error.name) + "\nMessage: " + 
                escapeTelegram(error.message) + "\n\nTokens were NOT requested.", true);
            
            // Reset trigger item
            items.getItem(tokenRequestSwitch).postUpdate("OFF");
        }
    }
});

2. openHAB MQTT broker and generic thing

Now that you created the Client_ID and tokens, we can create the MQTT broker and generic thing.
I did this through the main UI.

New thing → MQTT binding → MQTT broker

  1. Set your desired UID, i just used bmw (mqtt:broker:bmw)
  2. Set a laben that works for you, i used “BMW MQTT Broker”
  3. Broker hostname = customer.streaming-cardata.bmwgroup.com
  4. Port = 9000
  5. Secure connection = Yes
  6. Confirm hostname = Yes
  7. Protocol = TCP
  8. MQTT Version = 5
  9. Service quality = atleast once (1)
  10. Client-ID = Your Client_ID or empty (openHAB would create a Client_ID for you)
  11. Username = Your gcid / username
  12. Password = Your ID_Token
  13. Discovery = No ← Important!
  14. Save

New thing → MQTT binding → Generic MQTT thing

  1. Set your desired UID
  2. Set your desired label
  3. Create a new channel
    3.1 UID = mileage
    3.2 Label = Odometer
    3.3 MQTT state topic = < Your username>/< Your VIN>
    e.g. 12345678-1234-1234-123456789012/WBA1AB23456CD78901
    3.4 Unit of Measurement = km / mi
    3.5 Incoming Value Transformation = JSONPATH:$.data["vehicle.vehicle.travelledDistance"].value
  4. Link or create an item of your choice to this channel
  5. Done → Save

3. Setup the token update rules

Now we need to setup a rule that will refresh our tokens and input the ID_Token as new password into our MQTT broker thing.

3.1 Items to store your tokens

Before we can refresh the tokens, we need to create items that store our tokens between the refreshes.
I also added a switch to manually trigger a refresh which is mostly for testing and see if things work as intended.
Items:

String carBMW_Token_Access "Access Token [%s]" (gPersist, gBMWTokens)
String carBMW_Token_Refresh "Refresh Token [%s]" (gPersist, gBMWTokens)
String carBMW_Token_ID "ID Token [%s]" (gPersist, gBMWTokens)
String carBMW_Client_ID "Client ID [%s]" (gPersist, gBMWTokens)
String carBMW_Username "Username [%s]" (gPersist, gBMWTokens)
String carBMW_Code_Device "Device Code [%s]" (gPersist, gBMWTokens)
String carBMW_Code_User "User Code [%s]" (gPersist, gBMWTokens)
String carBMW_Code_Challenge "Challenge Code [%S]" (gPersist, gBMWTokens)
String carBMW_Code_Verifier "Verifier Code [%S]" (gPersist, gBMWTokens)
Switch carBMW_Token_Refresh_Trigger "Manual Token update" (gBMWTokens)
Switch carBMW_Token_Request "Start Token Request" (gBMWTokens)
Number carBMW_Expire "Expiry date" (gPersist, gBMWTokens)

I know that it might not be the best idea to store the password as plain text in an item, but i´m currently not aware of better ideas.

3.2 openHAB API Token

As the rule to update the password in the MQTT broker thing uses the Rest API, we need to create an API token first.
Open the Main UI → Login → Click on your user → Scroll down to API-Token → Create new API token
Enter the credentials of your user, choose a name for the token and create it.
Now copy the token and store it inside of the variable ‘apiToken’ in line 8 of ‘BMW_ID_Token_Update.js’

3.3 Refresh rule

I like to work with file based rules and created “BMW_Token_Refresh.js” in automation/js.
This rule will update the tokens 5 minutes before they expire.
You can enter your Telegram bot ID and use the global switches to enable or disable notifications.
I used the success message to make sure the tokens will be refreshed and now only use the error notification.

BMW_Token_Refresh.js
Enter your Telegram Bot ID in line 23 / 24!

/**
 * BMW Token Refresh Rule
 * Automatically updates Access Token, ID Token and Refresh Token
 * WITH SECURITY CHECKS - Tokens are ONLY updated on successful response
 * Last update: 26.10.2025 - 12:20
 */


// Global configuration
var TELEGRAM_NOTIFY_SUCCESS = false; // Send Telegram notification on successful token refresh
var TELEGRAM_NOTIFY_ERROR = true; // Send Telegram notification on errors
var RETRY_DELAY_SECONDS = 30; // Time before doing a retry when the first try fails
var isRetryAttempt = false; // Track if this is a retry

// Item configuration
var refreshTokenExpiryItem = 'carBMW_Expire';
var clientIDItem = 'carBMW_Client_ID';
var tokenAccessItem = 'carBMW_Token_Access';
var tokenRefreshItem = 'carBMW_Token_Refresh';
var tokenIdItem = 'carBMW_Token_ID';
var tokenRefreshTriggerSwitch = 'carBMW_Token_Refresh_Trigger';

// Telegram configuration
var bot1 = 1234; // Enter your telegram bot ID
var tgAddOnId = 'telegram:telegramBot:XYZ'; // Your telegram binding ID

//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
// please don't change the code from here
//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
var telegramAction = actions.get('telegram', tgAddOnId);
var refreshTokenExpiry = items.getItem(refreshTokenExpiryItem); // Item that stores the expiry time
// Function to escape special characters for Telegram
function escapeTelegram(text) {
    if (text === null || text === undefined) {
        return "";
    }
    text = text.toString();
    if (text.includes('{') || text.includes('[')) {
        return '`' + text + '`';
    }
    return text.replace(/[_*`[\]]/g, '\\$&');
    };

// Function to schedule retry
function scheduleRetry() {
    var logger = log("BMW_Token_Refresh");
    
    logger.warn("Scheduling retry in " + RETRY_DELAY_SECONDS + " seconds...");
    
    try {
        var retryTimer = actions.ScriptExecution.createTimer(
            java.time.ZonedDateTime.now().plusSeconds(RETRY_DELAY_SECONDS),
            function() {
                isRetryAttempt = true;
                performTokenRefresh();
            }
        );
    } catch (error) {
        logger.error("Error scheduling retry: " + error);
    }
}

// Check the expire time of the token
function isStillValid(timestamp) {
    var logger = log("BMW_Token_Refresh_ExpireCheck");
    var currentTime = Date.now();
  
    logger.info('current time: ' + formatDate(currentTime) + ', expiry of token: ' + formatDate(timestamp));
    
    if (currentTime < timestamp) {
        return true;
    } else {
        return false;
    }
}

// Convert EPOCH to DateTime
function formatDate(epochMillis) {
    var date = new Date(epochMillis);

    var day = String(date.getDate()).padStart(2, '0');
    var month = String(date.getMonth() + 1).padStart(2, '0');
    var year = date.getFullYear();
    var hours = String(date.getHours()).padStart(2, '0');
    var minutes = String(date.getMinutes()).padStart(2, '0');
    var seconds = String(date.getSeconds()).padStart(2, '0');

    return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
}

// Main refresh function
function performTokenRefresh() {
    var logger = log("BMW_Token_Refresh");
    
    if (isRetryAttempt) {
        logger.info("=== RETRY ATTEMPT - Second try after initial failure ===");
    }
    
    try {
        // Read tokens and Client ID from items
        var refreshToken = items.getItem(tokenRefreshItem).state.toString();
        var clientId = items.getItem(clientIDItem).state.toString();
        
        // Check if tokens are present
        if (refreshToken === "NULL" || refreshToken === "UNDEF" || refreshToken === "" ||
            clientId === "NULL" || clientId === "UNDEF" || clientId === "") {
            logger.warn("Refresh Token or Client ID not set. Please configure initially.");
            
            if (isRetryAttempt && telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                telegramAction.sendTelegram(bot1, "⚠️ BMW Token Refresh Failed (after retry)\n\n" +
                    "Refresh Token or Client ID not set. Please configure initially.");
            }
            
            // Reset retry flag
            isRetryAttempt = false;
            return;
        }
        
        logger.info("Starting token refresh...");
        
        // Prepare HTTP POST request
        var url = "https://customer.bmwgroup.com/gcdm/oauth/token";
        var contentType = "application/x-www-form-urlencoded";
        var payload = "grant_type=refresh_token&refresh_token=" + refreshToken + "&client_id=" + clientId;
        var headers = {};
        var timeout = 10000;
        
        // Execute HTTP request
        var response = actions.HTTP.sendHttpPostRequest(url, contentType, payload, headers, timeout);
        
        // CRITICAL: Check if response is valid
        if (response === null || response === undefined || response === "") {
            logger.error("No valid response received from BMW. Tokens will NOT be updated!");
            
            if (isRetryAttempt) {
                // Second attempt failed - send notification
                if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                    telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed\n\n" +
                        "No valid response received from BMW after retry.\n\n" +
                        "Tokens were NOT updated.\n\n" +
                        "Next automatic attempt in 30 minutes.");
                }
                isRetryAttempt = false;
            } else {
                // First attempt failed - schedule retry
                logger.warn("First attempt failed. Will retry in " + RETRY_DELAY_SECONDS + " seconds.");
                scheduleRetry();
            }
            return;
        }
        
        // Parse JSON response
        var jsonResponse = JSON.parse(response);
        
        // CRITICAL: Check if an error was returned
        if (jsonResponse.error) {
            var errorMsg = jsonResponse.error_description || jsonResponse.error;
            var errorCode = jsonResponse.error;
            
            logger.error("BMW API returned error");
            logger.error("Error Code: " + errorCode);
            logger.error("Error Description: " + errorMsg);
            logger.error("Full JSON Response: " + response);
            
            if (jsonResponse.error === "expired_token") {
                logger.error("====================================================");
                logger.error("REFRESH TOKEN HAS EXPIRED!");
                logger.error("====================================================");
                logger.error("ACTION REQUIRED: Please retrieve new Refresh Token manually from BMW!");
                
                // Always notify on expired token, even on first attempt
                if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                    telegramAction.sendTelegram(bot1, "🚨 BMW Token Refresh - CRITICAL\n\n" +
                        "REFRESH TOKEN HAS EXPIRED!\n\n" +
                        "Error Code: " + escapeTelegram(errorCode) + "\n" +
                        "Description: " + escapeTelegram(errorMsg) + "\n\n" +
                        "⚠️ ACTION REQUIRED:\n" +
                        "Please retrieve new Refresh Token manually from BMW and update " + tokenRefreshItem + "!");
                }
                isRetryAttempt = false;
                return;
            } else {
                // Other BMW API error
                if (isRetryAttempt) {
                    // Second attempt failed - send notification
                    if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                        telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed (after retry)\n\n" +
                            "BMW API Error\n\n" +
                            "Error Code: " + escapeTelegram(errorCode) + "\n" +
                            "Description: " + escapeTelegram(errorMsg) + "\n\n" +
                            "Tokens were NOT updated.\n\n" +
                            "Next automatic attempt in 30 minutes.");
                    }
                    isRetryAttempt = false;
                } else {
                    // First attempt failed - schedule retry
                    logger.warn("First attempt failed with BMW API error. Will retry in " + RETRY_DELAY_SECONDS + " seconds.");
                    scheduleRetry();
                }
                return;
            }
        }
        
        // CRITICAL: Check if all required fields are present
        if (!jsonResponse.access_token || !jsonResponse.refresh_token || !jsonResponse.id_token) {
            logger.error("Incomplete token response received. Tokens will NOT be updated!");
            logger.error("Full Response: " + response);
            
            if (isRetryAttempt) {
                // Second attempt failed - send notification
                if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                    telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed (after retry)\n\n" +
                        "Incomplete token response received.\n\n" +
                        "Tokens were NOT updated.");
                }
                isRetryAttempt = false;
            } else {
                // First attempt failed - schedule retry
                logger.warn("First attempt failed with incomplete response. Will retry in " + RETRY_DELAY_SECONDS + " seconds.");
                scheduleRetry();
            }
            return;
        }
        
        // Check if tokens are not empty or undefined
        if (jsonResponse.access_token === "" || jsonResponse.access_token === null ||
            jsonResponse.refresh_token === "" || jsonResponse.refresh_token === null ||
            jsonResponse.id_token === "" || jsonResponse.id_token === null) {
            logger.error("Received tokens are empty. Tokens will NOT be updated!");
            
            if (isRetryAttempt) {
                // Second attempt failed - send notification
                if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                    telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed (after retry)\n\n" +
                        "Received tokens are empty.\n\n" +
                        "Tokens were NOT updated.");
                }
                isRetryAttempt = false;
            } else {
                // First attempt failed - schedule retry
                logger.warn("First attempt failed with empty tokens. Will retry in " + RETRY_DELAY_SECONDS + " seconds.");
                scheduleRetry();
            }
            return;
        }
        
        // ONLY if all checks are successful: Update tokens
        items.getItem(tokenAccessItem).postUpdate(jsonResponse.access_token);
        items.getItem(tokenRefreshItem).postUpdate(jsonResponse.refresh_token);
        items.getItem(tokenIdItem).postUpdate(jsonResponse.id_token);

        // Calculate expiry time of the tokens minus 5 minutes
        var expireDateTime = Date.now() + (parseInt(jsonResponse.expires_in) * 1000) - 5*60*1000;
        var readableTime = formatDate(expireDateTime);

        // Safe the expiry time into the item
        refreshTokenExpiry.postUpdate(expireDateTime);
        logger.info("Token valid until: " + readableTime.toString());
        
        if (isRetryAttempt) {
            logger.info("Tokens successfully updated on RETRY attempt.");
        } else {
            logger.info("Tokens successfully updated.");
        }
        
        // Send success notification
        if (telegramAction !== null && TELEGRAM_NOTIFY_SUCCESS) {
            var successMsg = "✅ BMW Token Refresh Successful\n\nAll tokens have been updated successfully.";
            if (isRetryAttempt) {
                successMsg = "✅ BMW Token Refresh Successful (on retry)\n\n" +
                    "First attempt failed, but retry was successful.\n\n" +
                    "All tokens have been updated.";
            }
            telegramAction.sendTelegram(bot1, successMsg);
        }
        
        // Reset retry flag
        isRetryAttempt = false;
        
    } catch (error) {
        logger.error("Exception during token refresh: " + error);
        logger.error("Error type: " + error.name);
        logger.error("Error message: " + error.message);
        logger.error("Stack trace: " + error.stack);
        logger.error("Tokens were NOT changed - old tokens remain preserved.");
        
        if (isRetryAttempt) {
            // Second attempt failed - send notification
            if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Exception (after retry)\n\n" +
                    "Exception occurred during refresh.\n\n" +
                    "Error: " + escapeTelegram(error.toString()) + "\n" +
                    "Type: " + escapeTelegram(error.name) + "\n" +
                    "Message: " + escapeTelegram(error.message) + "\n\n" +
                    "Tokens were NOT changed.\n\n" +
                    "Next automatic attempt in 30 minutes.");
            }
            isRetryAttempt = false;
        } else {
            // First attempt failed - schedule retry
            logger.warn("First attempt failed with exception. Will retry in " + RETRY_DELAY_SECONDS + " seconds.");
            scheduleRetry();
        }
    }
}

// Automatic refresh rule - runs every minute
rules.JSRule({
    name: "BMW Token Refresh",
    description: "Automatically updates BMW tokens 5 minutes before they expire",
    triggers: [
        triggers.GenericCronTrigger("0 * * * * ?")  // Every minute
    ],
    execute: function(event) {
        var logger = log("BMW_Token_Refresh");

        // Import the current expire time
        var refreshTokenExpiryTime = parseInt(refreshTokenExpiry.state);

        if (isNaN(refreshTokenExpiryTime)) {
            logger.warn(refreshTokenExpiryItem + " is not a valid timestamp: " + refreshTokenExpiry.state);
        } else if (isStillValid(refreshTokenExpiryTime)) {
            logger.info("The access token is still valid -> wait");
        } else {
            logger.info("Token needs to be updated! -> Starting update");
            isRetryAttempt = false;
            performTokenRefresh();
        }

    }
});

// Manual refresh via switch
rules.JSRule({
    name: "BMW Token Refresh - Manual",
    description: "Manual token refresh via switch",
    triggers: [
        triggers.ItemCommandTrigger(tokenRefreshTriggerSwitch, "ON")
    ],
    execute: function(event) {
        var logger = log("BMW_Token_Refresh_Manual");
        
        try {
            logger.info("Manual token refresh triggered...");
            
            // Reset retry flag to ensure clean state for manual refresh
            isRetryAttempt = false;
            
            // Call the same function as automatic refresh
            performTokenRefresh();
            
        } catch (error) {
            logger.error("Error triggering manual token refresh: " + error);
            logger.error("Stack trace: " + error.stack);
            
            // Send error notification
            if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
                telegramAction.sendTelegram(bot1, "❌ BMW Manual Token Refresh Exception\n\n" +
                    "Could not trigger manual refresh.\n\n" +
                    "Exception: " + escapeTelegram(error.toString()));
            }
        } finally {
            // Always turn off the trigger switch
            items.getItem(tokenRefreshTriggerSwitch).postUpdate("OFF");
        }
    }
});

3.4 Token update rule

This rule will update the password of your MQTT broker thing once the id_token was refreshed.
"BMW_ID_Token_Update.js in automation/js

`BMW_ID_Token_Update.js`
Please enter your apiToken and MQTT broker thing ID in line 8 and 10!

/**
 * MQTT Broker Password Update Rule
 * Automatically updates the MQTT broker password when the ID token is refreshed
 * Uses REST API with authentication for openHAB 5.x
 */

// Global configuration
var apiToken = 'oh.MQTTUpdateToken.YgthU3GH9f8ef5JOqca4JuAkutAW77k2skMuNfEL6MUabaKpAPRb8f28PzNtianZ2VoOXeVHQTxQbUGHvPLg'; // Enter your openHAB API token
var bmwTokenItemName = 'carBMW_Token_ID'; // Enter your Item that stores the ID token aka MQTT password
var usernameItem = 'carBMW_Username'; // Enter your Item that stores the gcid aka MQTT username
var thingUID = "mqtt:broker:bmw"; // Enter the UID of your MQTT broker thing

//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
// please don't change the code from here
//------------------------------------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Function to check if the MQTT broker thing is online
function isThingOffline(thing) {
  var thing = osgi.getService("org.openhab.core.thing.ThingRegistry").get(new org.openhab.core.thing.ThingUID(thing));
  return thing && thing.getStatus().toString() === "ONLINE";
}
// Helper function to update username in MQTT thing
function updateMqttUsername(thingConfig, thingUID, headers, newUsername, logger) {
    thingConfig.configuration.username = newUsername;
    var putUrl = "http://localhost:8080/rest/things/" + thingUID;
    var response = actions.HTTP.sendHttpPutRequest(
        putUrl,
        "application/json",
        JSON.stringify(thingConfig),
        headers,
        5000
    );
    return response !== null;
}
//------------------------------------------------------------------------------------------------------------------------------------------------------------
rules.JSRule({
    name: "Update BMW MQTT Password",
    description: "Updates BMW MQTT broker password when ID token changes",
    triggers: [
        triggers.ItemStateChangeTrigger(bmwTokenItemName)
    ],
    execute: function(event) {
        var logger = log("BMW_MQTT_Password_Update");
        
        try {
            var newToken = items.getItem(bmwTokenItemName).state.toString();
            var usernameItemState = items.getItem(usernameItem).state.toString();
            var newUsername = (usernameItemState === null || usernameItemState.toString() === "NULL" || usernameItemState.toString() === "UNDEF") ? "" : usernameItemState.toString();
            
            // Check if token is valid
            if (newToken === "NULL" || newToken === "UNDEF" || newToken === "") {
                logger.warn("ID Token is not valid, skipping MQTT password update");
                return;
            }
            
            logger.debug("Updating MQTT broker password with new ID token...");
                        
            // Prepare headers
            var headers = {
                "Content-Type": "application/json",
                "Accept": "application/json"
            };
            
            // Add authentication if API token is available
            if (apiToken !== null) {
                headers["Authorization"] = "Bearer " + apiToken;
            }
            
            // Get current Thing configuration via REST API
            var getUrl = "http://localhost:8080/rest/things/" + thingUID;
            var thingJson = actions.HTTP.sendHttpGetRequest(getUrl, headers, 5000);
            
            if (thingJson === null || thingJson === "") {
                logger.error("Could not retrieve Thing configuration");
                return;
            }
            
            var thingConfig = JSON.parse(thingJson);

            if (newUsername === "NULL" || newUsername === "UNDEF" || newUsername === "") {
                logger.warn("No valid username in item " + usernameItem);
            } else {
                var currentUsername = thingConfig.configuration.username;
                
                if (!currentUsername || currentUsername === "" || currentUsername === "null") {
                    logger.info("Setting initial username: " + newUsername);
                    if (updateMqttUsername(thingConfig, thingUID, headers, newUsername, logger)) {
                        logger.info("Username successfully set.");
                    } else {
                        logger.error("Failed to set username.");
                    }
                } else if (currentUsername !== newUsername) {
                    logger.info("Updating username: " + currentUsername + " -> " + newUsername);
                    if (updateMqttUsername(thingConfig, thingUID, headers, newUsername, logger)) {
                        logger.info("Username successfully updated.");
                    } else {
                        logger.error("Failed to update username.");
                    }
                } else {
                    logger.info("Username unchanged: " + currentUsername);
                }
            }
            
            // Update password in configuration
            thingConfig.configuration.password = newToken;
            
            // Send updated configuration back via REST API
            var putUrl = "http://localhost:8080/rest/things/" + thingUID;
            
            var response = actions.HTTP.sendHttpPutRequest(
                putUrl,
                "application/json",
                JSON.stringify(thingConfig),
                headers,
                5000
            );
            
            logger.debug("MQTT broker password successfully updated with new ID token");
            
        } catch (error) {
            logger.error("Error updating MQTT broker password: " + error);
        }
    }
});
//------------------------------------------------------------------------------------------------------------------------------------------------------------
// Optional: Initial setup rule to set password on openHAB startup
rules.JSRule({
    name: "Set BMW MQTT Password",
    description: "Sets BMW MQTT broker password from ID token on system startup",
    triggers: [
        triggers.SystemStartlevelTrigger(100)
    ],
    execute: function(event) {
        var logger = log("BMW_MQTT_Password_Startup");

        if(isThingOffline(thingUID)) {
            logger.debug("MQTT Thing online, no update needed.")
            return;
        }
        
        try {
            // Wait a bit for system to be fully ready
            java.lang.Thread.sleep(10000);
            
            var idToken = items.getItem(bmwTokenItemName).state.toString();
            
            if (idToken === "NULL" || idToken === "UNDEF" || idToken === "") {
                logger.warn("ID Token not available on startup");
                return;
            }
                        
            logger.debug("Setting MQTT broker password from stored ID token...");
                        
            // Prepare headers
            var headers = {
                "Content-Type": "application/json",
                "Accept": "application/json"
            };
            
            // Add authentication if API token is available
            if (apiToken !== null) {
                headers["Authorization"] = "Bearer " + apiToken;
            }
            
            // Get current Thing configuration via REST API
            var getUrl = "http://localhost:8080/rest/things/" + thingUID;
            var thingJson = actions.HTTP.sendHttpGetRequest(getUrl, headers, 5000);
            
            if (thingJson === null || thingJson === "") {
                logger.error("Could not retrieve Thing configuration on startup");
                return;
            }
            
            var thingConfig = JSON.parse(thingJson);
            
            // Update password in configuration
            thingConfig.configuration.password = idToken;
            
            // Send updated configuration back via REST API
            var putUrl = "http://localhost:8080/rest/things/" + thingUID;
            
            var response = actions.HTTP.sendHttpPutRequest(
                putUrl,
                "application/json",
                JSON.stringify(thingConfig),
                headers,
                5000
            );
            
            logger.debug("MQTT broker password set successfully on startup");
            
        } catch (error) {
            logger.error("Error setting MQTT broker password on startup: " + error);
        }
    }
});

Test your configuration

Once you´re done, you can check your configuration by sending a command to your BMW through the MyBMW app, e.g. lock the car.
This should trigger a new message and your mileage item should receive the current mileage.

4. Create more channels

Once your test was successful, you can start to add more channels for your datapoints.
To find out which datapoints your car uses, you can create a channel without the input value transformation and link it to a text item.
String carBMW_RAW_Data "Raw data [%s]" (gPersist) {channel="mqtt:topic:bmw:m340:raw-data"}
Open the log viewer, and filter for carBMW_RAW_Data' updated, trigger another update through the MyBMW app and wait for the logs.
Now you can save them as csv, use Excel to dismiss double values and see what´s available for your car.

Changelog

  • 0.1 - 06.10.2025
    Initial concept

  • 0.2 - 07.10.2025
    The periodic refresh failed after a few hours with an exception “error”
    Added a “retry once” strategy and more logging

  • 0.3 - 07.10.2025
    Added a replace to catch _ as character that can´t be sent with Telegram

  • 0.4 - 07.10.2025
    Added better logging and reworked reschedule timer.

  • 0.5 - 08.10.2025
    I switched back to a cronjob and hopefully this will make the rule more reliable

  • 0.6 - 23.10.2025
    Added a header to the sendHttpPostRequest

  • 0.7 - 23.10.2025
    Added the Setup rule to get your tokens with a Telegram flow.

  • 0.8 - 24.10.2025
    Thanks to MartinOpenhabFan i could change the logic for the refresh from every 30 minutes to 5 minutes before the tokens expire.

  • 0.9 - 25.10.2025
    Added variables to configure your personal telegram items.
    Added some more variables to configure your personal items.

  • 0.10 - 26.10.2025
    Added a check to the ID token update and the startup rule only runs if the MQTT broker thing is offline

  • 0.11 - 09.11.2025
    Added the logviewer mode to setup your tokens!

  • 0.12 - 10.11.2025
    Added the username as item and update the state in the setup and MQTT thing refresh


Contributors


Disclaimer: The rules were created with Claude AI and ChatGPT.

5 Likes

To do list

  • Clean the code and comments
  • Combine more rules into one
2 Likes

Awesome! Patiently waiting for that, since the ‘out of the box’ BMW procedure seems a lot of hassle just for reading out the SOC of my partner’s i3 :wink:

1 Like

I‘m currently in greek and couldn‘t finish it before my holiday.
I‘ll be back next week and post the first version.

Hi Michael, really great that you created the scripts. I follow a slightly different approach based on your examples, I configured everything via Main UI.

I’m storing the expiry timestamp in a variable and run the script each Minute and only if the expire timestamp is less than 5mins away, then I trigger the refresh. It runs since some days reliably.

There is just one issue in your script in the POST request to trigger the token refresh, the method signature has the header map at fourth param and the timeout as fifth. This issue caused an error with each second request. I fixed it by adding a header and now it runs without issues.

Do you see any benefit in running the refresh just before the tokens expire?
My 30 minute refresh now runs reliable and the issues were in the backend of BMW.

I need to check if i updated the post to my latest version. As it‘s working, i‘m not sure if it‘s really wrong or just a different approach :slight_smile:

No, except the issue in the POST request there’s nothing wrong, just a different approach. I just wanted to utilize the expiry time.

Is it written in JS and could you share it?

Sure I’m just busy until the weekend.

1 Like

Thanks for this new connection. I did the steps and got all the details but can’t get it working with MQTTX. I get the bad username or password. I used for Password the id_token what you mentioned but not sure about the username. I tried different options but also the username shown at the CARDATA-STREAM but with the same result. Any suggestions?

Your username is the gcid from the last step.

I have MQTTX working, the broker is online and I created a things file and added the items. I also set the org.openhab.binding.mqtt.generic in trace mode but I don’t receive any data. How to debug this?

Bridge mqtt:broker:1213a7a8b1 [ ] 
{
  Thing topic bmwCar "BMW iX3 Datastream" {
     Channels:
      Type number : lastRemainingRange [
        stateTopic="bba7b916-ebae-43d3-bc23-cc7bd1f18f7c/XBY7X410X0S130998/vehicle/drivetrain/lastRemainingRange",
        transformationPattern="JSONPATH:$.value"
      ]
      Type number : avgElectricRangeConsumption [
        stateTopic="bba7b916-ebae-43d3-bc23-cc7bd1f18f7c/XBY7X410X0S130998/vehicle/drivetrain/avgElectricRangeConsumption",
        transformationPattern="JSONPATH:$.value"
      ]
      Type string : debugAll [
        stateTopic="bba7b916-ebae-43d3-bc23-cc7bd1f18f7c/XBY7X410X0S130998/#"
      ]
      Type string : debugAllState [
         stateTopic="bba7b916-ebae-43d3-bc23-cc7bd1f18f7c/#"
      ]
  }
}

and

Number BMWlastRemainingRange "Actueel bereik [%.0f km]" { channel="mqtt:topic:bmwCar:lastRemainingRange" }
Number BMWAvgElectricRangeConsumption "Gemiddeld verbruik [%.2f kWh/100km]" { channel="mqtt:topic:bmwCar:avgElectricRangeConsumption" }
String BMWRawData "BMW Debug [%s]" { channel="mqtt:topic:bmwCar:debugAll" }
String BMWRawDataState "BMW Debug [%s]" { channel="mqtt:topic:bmwCar:debugAllState" }


You wont receive any updates unless your car is sending new data.

You can force an update through the MyBMW app, just send a command like lock or light to the car.

Yes I know but expect when MQTTX receives date I would also see date in OH.

You can‘t have MQTTX and openHAB broker online at the same time with the same credentials.

New update added with rule to do the initial setup through telegram messages.

Edit: Another update added and a huge shoutout to @MartinOpenhabFan for his input and sharing his approach with me.

1 Like

Many thanks :slight_smile: ! Eager to try this out immediately, I went straight to “1.2 openHAB Telegram” and did steps 1, 2 and 3. I suppose after that i have to run the provided rule with my Telegram Bot ID in line 9, correct? When i do that, openhab log says:

22:43:21.846 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Step 1: Device Code Request22:43:21.869 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Handle Telegram Reply22:43:21.879 [WARN ] [odule.handler.ItemStateTriggerHandler] - Item ‘tgReplyId’ needed for rule ‘BMW-Token-Setup—Handle-Telegram-Reply-code_altered_just_for_sure’ is missing. Trigger ‘other_code_altered_just_for_sure’ will not work.22:43:21.893 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Step 2: Token Request

Nothing is arriving via Telegram. Am I missing something here? I did the Telegram binding setup a long time ago and is otherwise functional for sending OH messages to my phone.

Ah i‘m sorry, you need to input your Telegram itemd for the replyId and lastMessageText.
I‘ll add that info and where to input the items.

Fixed :slight_smile:

You can now configure your personal telegram items in line 13 / 14.

Edit: Added some more variables to configure.

1 Like

Sorry but I’m possibly suffering from Telegram-related confusion :wink:

The rule says:
// Telegram configuration
var bot1 = 1234; // Enter your telegram bot ID
var tgAddOnId = ‘telegram:telegramBot:XYZ’; // Enter your telegram binding ID

I find in my OH Telegram Bot thingy:
Thing UID, i.e:
telegram:telegramBot:a1234bcd5e
Chat Id(s), i.e
123456789

Assuming that by “telegram bot ID” in “var bot1” is meant the Chat ID and the “telegram binding ID” is indeed the Thing UID, i think i have configured that correctly now. The error in OH log is gone, but I’m still not receiving any Telegram messages.

22:09:08.182 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Step 1: Device Code Request
22:09:08.210 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Handle Telegram Reply
22:09:08.234 [INFO ] [g.openhab.automation.openhab-js.rules] - Adding rule: BMW Token Setup - Step 2: Token Request