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.
- Create a Client_ID and tokens in the BMW portals
- 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
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
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)
Switch carBMW_Token_Refresh_Trigger "Manually update tokens" (gBMWTokens)
String carBMW_API_Token "oH API Token" (gPersist)
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 carBMW_API_Token
Maybe there are better ways to do this i´m not aware of…
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 15 seconds after startup and then every 50 minutes.
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 Rule
* Automatically updates Access Token, ID Token and Refresh Token
* WITH SECURITY CHECKS - Tokens are ONLY updated on successful response
*/
var REFRESH_INTERVAL_MINUTES = 50; // Refresh tokens every 50 minutes (before expiry after 60 min)
var TELEGRAM_NOTIFY_SUCCESS = true; // Send Telegram notification on successful token refresh
var TELEGRAM_NOTIFY_ERROR = true; // Send Telegram notification on errors
// Timer for automatic refresh
var tokenRefreshTimer = null;
// Function to schedule next refresh
function scheduleNextRefresh() {
var logger = log("BMW_Token_Refresh");
// Cancel existing timer if any
if (tokenRefreshTimer !== null) {
tokenRefreshTimer.cancel();
}
// Schedule next refresh in REFRESH_INTERVAL_MINUTES
tokenRefreshTimer = actions.ScriptExecution.createTimer(
java.time.ZonedDateTime.now().plusMinutes(REFRESH_INTERVAL_MINUTES),
function() {
performTokenRefresh();
}
);
logger.info("Next token refresh scheduled in " + REFRESH_INTERVAL_MINUTES + " minutes");
}
// Main refresh function
function performTokenRefresh() {
var logger = log("BMW_Token_Refresh");
// Get Telegram action
var telegramAction = actions.get("telegram", "telegram:telegramBot:bot");
var bot1 = <Your Telegram bot ID>;
try {
// Read tokens and Client ID from items
var refreshToken = items.getItem("carBMW_Token_Refresh").state.toString();
var clientId = items.getItem("carBMW_Client_ID").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 (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "⚠️ BMW Token Refresh\n\nRefresh Token or Client ID not set. Please configure initially.");
}
// Schedule next attempt
scheduleNextRefresh();
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;
// Execute HTTP request
var response = actions.HTTP.sendHttpPostRequest(url, contentType, payload, 10000);
// 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 (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed\n\nNo valid response received from BMW. Tokens were NOT updated.");
}
// Schedule next attempt
scheduleNextRefresh();
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;
if (jsonResponse.error === "expired_token") {
logger.error("====================================================");
logger.error("REFRESH TOKEN HAS EXPIRED!");
logger.error("Error message: " + errorMsg);
logger.error("====================================================");
logger.error("ACTION REQUIRED: Please retrieve new Refresh Token manually from BMW and save it in carBMW_Token_Refresh!");
logger.error("Tokens were NOT updated - old tokens remain preserved.");
if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "🚨 BMW Token Refresh - CRITICAL\n\n" +
"REFRESH TOKEN HAS EXPIRED!\n\n" +
"Error: " + errorMsg + "\n\n" +
"⚠️ ACTION REQUIRED:\n" +
"Please retrieve new Refresh Token manually from BMW and update carBMW_Token_Refresh!");
}
} else {
logger.error("BMW API Error: " + jsonResponse.error);
logger.error("Description: " + errorMsg);
logger.error("Tokens were NOT updated.");
if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed\n\n" +
"BMW API Error: " + jsonResponse.error + "\n\n" +
"Description: " + errorMsg + "\n\n" +
"Tokens were NOT updated.");
}
}
// Schedule next attempt
scheduleNextRefresh();
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("Response: " + response);
if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed\n\nIncomplete token response received. Tokens were NOT updated.");
}
// Schedule next attempt
scheduleNextRefresh();
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 (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Failed\n\nReceived tokens are empty. Tokens were NOT updated.");
}
// Schedule next attempt
scheduleNextRefresh();
return;
}
// ONLY if all checks are successful: Update tokens
items.getItem("carBMW_Token_Access").postUpdate(jsonResponse.access_token);
items.getItem("carBMW_Token_Refresh").postUpdate(jsonResponse.refresh_token);
items.getItem("carBMW_Token_ID").postUpdate(jsonResponse.id_token);
logger.info("Tokens successfully updated. Next update in " + REFRESH_INTERVAL_MINUTES + " minutes.");
// Send success notification
if (telegramAction !== null && TELEGRAM_NOTIFY_SUCCESS) {
telegramAction.sendTelegram(bot1, "✅ BMW Token Refresh Successful\n\nAll tokens have been updated successfully.");
}
// Schedule next refresh
scheduleNextRefresh();
} catch (error) {
logger.error("Error during token refresh: " + error);
logger.error("Tokens were NOT changed - old tokens remain preserved.");
// Send error notification
var telegramAction = actions.get("telegram", "telegram:telegramBot:bot");
if (telegramAction !== null && TELEGRAM_NOTIFY_ERROR) {
telegramAction.sendTelegram(bot1, "❌ BMW Token Refresh Error\n\n" +
"Exception: " + error + "\n\n" +
"Tokens were NOT changed - old tokens remain preserved.");
}
// Schedule next attempt
scheduleNextRefresh();
}
}
// Initial startup rule
rules.JSRule({
name: "BMW Token Refresh - Startup",
description: "Starts automatic token refresh on system startup",
triggers: [
triggers.SystemStartlevelTrigger(100)
],
execute: function(event) {
var logger = log("BMW_Token_Refresh_Startup");
try {
// Wait for system to be ready
java.lang.Thread.sleep(15000);
logger.info("Starting BMW token refresh timer...");
// Perform initial refresh
performTokenRefresh();
} catch (error) {
logger.error("Error starting BMW token refresh: " + error);
}
}
});
// Manual refresh via switch (optional)
rules.JSRule({
name: "BMW Token Refresh - Manual",
description: "Manual token refresh via switch",
triggers: [
triggers.ItemCommandTrigger("carBMW_Token_Refresh_Trigger", "ON")
],
execute: function(event) {
var logger = log("BMW_Token_Refresh_Manual");
try {
var refreshToken = items.getItem("carBMW_Token_Refresh").state.toString();
var clientId = items.getItem("carBMW_Client_ID").state.toString();
if (refreshToken === "NULL" || refreshToken === "UNDEF" || refreshToken === "" ||
clientId === "NULL" || clientId === "UNDEF" || clientId === "") {
logger.warn("Refresh Token or Client ID not set.");
items.getItem("carBMW_Token_Refresh_Trigger").postUpdate("OFF");
return;
}
logger.info("Manual token refresh started...");
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 response = actions.HTTP.sendHttpPostRequest(url, contentType, payload, 10000);
// 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!");
items.getItem("carBMW_Token_Refresh_Trigger").postUpdate("OFF");
return;
}
var jsonResponse = JSON.parse(response);
// CRITICAL: Check if an error was returned
if (jsonResponse.error) {
if (jsonResponse.error === "expired_token") {
logger.error("====================================================");
logger.error("REFRESH TOKEN HAS EXPIRED!");
logger.error("Error message: " + jsonResponse.error_description);
logger.error("====================================================");
logger.error("ACTION REQUIRED: Please retrieve new Refresh Token manually from BMW and save it in carBMW_Token_Refresh!");
logger.error("Tokens were NOT updated - old tokens remain preserved.");
} else {
logger.error("BMW API Error: " + jsonResponse.error);
logger.error("Description: " + jsonResponse.error_description);
logger.error("Tokens were NOT updated.");
}
items.getItem("carBMW_Token_Refresh_Trigger").postUpdate("OFF");
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("Response: " + response);
items.getItem("carBMW_Token_Refresh_Trigger").postUpdate("OFF");
return;
}
// Check if tokens are not empty
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!");
items.getItem("carBMW_Token_Refresh_Trigger").postUpdate("OFF");
return;
}
// ONLY if all checks are successful: Update tokens
items.getItem("carBMW_Token_Access").postUpdate(jsonResponse.access_token);
items.getItem("carBMW_Token_Refresh").postUpdate(jsonResponse.refresh_token);
items.getItem("carBMW_Token_ID").postUpdate(jsonResponse.id_token);
logger.info("Manual token refresh completed successfully.");
} catch (error) {
logger.error("Error during manual token refresh: " + error);
logger.error("Tokens were NOT changed - old tokens remain preserved.");
} finally {
items.getItem("carBMW_Token_Refresh_Trigger").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
You need to enter the UID of your MQTT broker thing in both parts of the rule.
I used mqtt:broker:bmw
/**
* 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
*/
rules.JSRule({
name: "Update BMW MQTT Password",
description: "Updates BMW MQTT broker password when ID token changes",
triggers: [
triggers.ItemStateChangeTrigger("carBMW_Token_ID")
],
execute: function(event) {
var logger = log("MQTT_Password_Update");
try {
var newToken = items.getItem("carBMW_Token_ID").state.toString();
// Check if token is valid
if (newToken === "NULL" || newToken === "UNDEF" || newToken === "") {
logger.warn("ID Token is not valid, skipping MQTT password update");
return;
}
// Get API token if available
var apiTokenItem = items.getItem("carBMW_API_Token");
var apiToken = null;
if (apiTokenItem !== null && apiTokenItem.state.toString() !== "NULL" &&
apiTokenItem.state.toString() !== "UNDEF" && apiTokenItem.state.toString() !== "") {
apiToken = apiTokenItem.state.toString();
}
logger.info("Updating MQTT broker password with new ID token...");
var thingUID = "mqtt:broker:bmw"; // <- Enter your UID
// 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");
logger.error("Please check if API token is set in item 'carBMW_API_Token'");
return;
}
var thingConfig = JSON.parse(thingJson);
// 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.info("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("MQTT_Password_Startup");
try {
// Wait a bit for system to be fully ready
java.lang.Thread.sleep(10000);
var idToken = items.getItem("carBMW_Token_ID").state.toString();
if (idToken === "NULL" || idToken === "UNDEF" || idToken === "") {
logger.warn("ID Token not available on startup");
return;
}
// Get API token if available
var apiTokenItem = items.getItem("carBMW_API_Token");
var apiToken = null;
if (apiTokenItem !== null && apiTokenItem.state.toString() !== "NULL" &&
apiTokenItem.state.toString() !== "UNDEF" && apiTokenItem.state.toString() !== "") {
apiToken = apiTokenItem.state.toString();
}
logger.info("Setting MQTT broker password from stored ID token...");
var thingUID = "mqtt:broker:bmw"; // <- Enter your UID
// 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.info("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.
Disclaimer: The rules were created with Claude AI.