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.
- Create a Client_ID and get your tokens
- Manual setup in the BMW portals
- Setup with openHAB and Telegram
- Setup with openHAB and logviewer
- Create a MQTT Broker and Generic thing
2.1. Create channels in the Genering thing for your items - Create the JSRules to refresh the tokens and update your Broker thing
- Create more channels

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.
-
Open the vehicle overview and select “BMW CarData”
https://www.bmw.de/de-de/mybmw/vehicle-overview -
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. -
Open the “CarData Customer Portal” and select “Device Code Flow API”
CarData Customer Portal -
Expand “Device Flow with PKCE” and then expand “gcdm/oauth/device/code” (Starts the device code flow)
-
Click “Try it out”, insert the Client_ID from step 2, and click Execute.
-
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. -
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”. -
Go back to the BMW CarData page, scroll down to “CarData Streaming”, and create a stream.
-
Expand the connection details and copy the username (gcid).
-
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. -
Go back to the “CarData Customer Portal” and expand “/gcdm/oauth/token” (Request a token for the device).
-
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. -
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
- Set your desired UID, i just used bmw (mqtt:broker:bmw)
- Set a laben that works for you, i used “BMW MQTT Broker”
- Broker hostname = customer.streaming-cardata.bmwgroup.com
- Port = 9000
- Secure connection = Yes
- Confirm hostname = Yes
- Protocol = TCP
- MQTT Version = 5
- Service quality = atleast once (1)
- Client-ID = Your Client_ID or empty (openHAB would create a Client_ID for you)
- Username = Your gcid / username
- Password = Your ID_Token
- Discovery = No ← Important!
- Save
New thing → MQTT binding → Generic MQTT thing
- Set your desired UID
- Set your desired label
- 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 - Link or create an item of your choice to this channel
- 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
- @MartinOpenhabFan
Logic to refresh the tokens 5 minutes before they expire
Disclaimer: The rules were created with Claude AI and ChatGPT.
