Hi,
I had some problems getting the solution from BredmichMichael working (I tried his first version a couple of months ago). I also looked at the Python solution, which was running fine, but I wanted a JavaScript-based approach. I used code from BredmichMichael and ideas from BucofskiTVDV.
Everything is contained in a single JS file.
Before you start, you need to have the MQTT and Telegram bindings installed and working.
From BMW you will need a Client ID, GCID, and streaming data access. Please follow the instructions from BredmichMichael in the openHAB Community thread: [openHAB Community] Connect to new BMW CarData service through MQTT .
Below you can find the required items.
// BMWClientID must be set before you can start.
// Request your Client ID at: https://www.bmw.de/de-de/mybmw/vehicle-overview
String BMWClientID "Client ID [%s]"
// All items below are initialized after triggering ManualTokenRefresh
String BMWTokenAccess "Access Token"
String BMWTokenRefresh "Refresh Token"
String BMWTokenID "ID Token"
String BMWCodeDevice "Device Code"
String BMWCodeUser "User Code"
String BMWCodeChallenge "Challenge Code"
String BMWCodeVerifier "Verifier Code"
Number BMWExpire "Expiry date"
Switch ManualTokenRefresh
The BMWMqtt.things file. You don’t need to configure anything in the GUI. The Broker and Datastream are created automatically. Search in this file for “add gcid/BMW ID LOGIN(email address)” and for “add gcid/VIN” (appearing multiple times), and replace these placeholders with your own values.
Bridge mqtt:broker:bmw "MQTT Broker iX3" [
lwtQos=0,
publickeypin=true,
keepAlive=60,
hostnameValidated=true,
secure=true,
birthRetain=true,
shutdownRetain=true,
certificatepin=true,
password="this field is automatiically filled",
protocol="TCP",
qos=1,
reconnectTime=60000,
port=9000,
mqttVersion="V5",
host="customer.streaming-cardata.bmwgroup.com",
lwtRetain=true,
username="add gcid/BMW ID LOGIN(email address)",
enableDiscovery=false
] {
Thing topic bmwCar "iX3 Datastream" {
Channels:
Type number : lastRemainingRange "Actieradius resterend" [
stateTopic="add gcid/VIN",
transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.lastRemainingRange\"].value"
]
Type number : timeToFullyCharged "Tijd tot vol" [
stateTopic="add gcid/VIN",
transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.electricEngine.charging.timeToFullyCharged\"].value"
]
Type number : batteryHeader "Accu status %" [
stateTopic="add gcid/VIN",
transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.batteryManagement.header\"].value"
]
Type number : travelledDistance "Kilometerstand" [
stateTopic="add gcid/VIN",
transformationPattern="JSONPATH:$.data[\"vehicle.vehicle.travelledDistance\"].value"
]
}
}
And the JS file. Search for BotXXXXX and replace it with your own Telegram bot account.
var { items, rules, triggers, actions, log } = require("openhab");
// -----------------------------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------------------------
const config = {
// === TELEGRAM ===
telegramCallback: "telegram:telegramBot:BotXXXXX:callbackRawEvent",
telegramActionUID: "telegram:telegramBot:BotXXXXX",
telegramReplyId: "TelegramBotXXXXReplyId",
bmwAuthenticatieId: "BMWAuthenticatie",
// === TOKEN ITEMS ===
tokenRefreshItem: "BMWTokenRefresh",
clientIDItem: "BMWClientID",
tokenAccessItem: "BMWTokenAccess",
refreshTokenExpiryItem: "BMWExpire",
codeVerifierItem: "BMWCodeVerifier",
codeChallengeItem: "BMWCodeChallenge",
deviceCodeItem: "BMWCodeDevice",
userCodeItem: "BMWCodeUser",
tokenIdItem: "BMWTokenID",
manualTriggerItem: "ManualTokenRefresh",
// === THINGS FILE / MQTT ===
manualUpdateItem: "ManualUpdateBMWThings",
mqttThingsFile: "/openhab/conf/things/BMWMqtt.things",
// === MISC ===
tokenEndpointHost: "customer.bmwgroup.com"
};
const HTTP_TIMEOUT = 15000;
let refreshTriggerTime = null;
let refreshTimer = null;
let refreshBeforeExpire = 1; // minutes
let pollingTimer = null;
let pollingIntervalMs = 5000;
let pollingStartTime = 0;
const MAX_POLLING_TIME_MS = 4 * 60 * 1000;
// -----------------------------------------------------------------------------
// LOGGER
// -----------------------------------------------------------------------------
const logger = log("BMWTokenManager");
const DEBUG = true;
function dbg(msg) { if (DEBUG) logger.info("[DEBUG] " + msg); }
// -----------------------------------------------------------------------------
// TELEGRAM SUPPORT
// -----------------------------------------------------------------------------
function getTelegramAction() {
const tg = actions.get("telegram", config.telegramActionUID);
if (!tg) logger.error("telegramAction is NULL - check Thing UID");
return tg;
}
function sendTelegramSafe(message) {
const tg = getTelegramAction();
if (tg) tg.sendTelegram(message);
}
function sendTelegramAnswerSafe(replyId, message) {
const tg = getTelegramAction();
if (tg) tg.sendTelegramAnswer(replyId, message);
}
// -----------------------------------------------------------------------------
// UTILITY FUNCTIONS
// -----------------------------------------------------------------------------
function encodeParam(key, value) {
const URLEncoder = Java.type("java.net.URLEncoder");
const StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
return URLEncoder.encode(key, StandardCharsets.UTF_8.name()) + "=" +
URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
function formatDate(epochMillis) {
const d = new Date(epochMillis);
const p = (v) => String(v).padStart(2, "0");
return `${p(d.getDate())}.${p(d.getMonth()+1)}.${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
function formatDateLong(epochMillis) { return new Date(epochMillis).toString(); }
function getItemState(name) {
try {
const it = items.getItem(name);
if (!it || !it.state || it.state == "NULL" || it.state == "UNDEF") return null;
return it.state.toString();
} catch (e) {
logger.error("getItemState error for " + name + ": " + e);
return null;
}
}
function buildTelegramText(url, exp) {
return ["Open the link below (first-time login):\n", url, "\n",
`This authorization is valid for ${exp} minutes.`].join("\n");
}
// -----------------------------------------------------------------------------
// SUBSYSTEM: DEVICE FLOW (Initial Authentication)
// -----------------------------------------------------------------------------
// PKCE generators
function generateCodeVerifier() {
const SecureRandom = Java.type("java.security.SecureRandom");
const Base64 = Java.type("java.util.Base64");
const random = new SecureRandom();
const bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 32);
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
function generateCodeChallenge(verifier) {
const MessageDigest = Java.type("java.security.MessageDigest");
const Base64 = Java.type("java.util.Base64");
const StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
const digest = MessageDigest.getInstance("SHA-256");
const hash = digest.digest(verifier.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
}
// Device code flow
function deviceCodeFlowAndAuthorisation() {
try {
const clientId = getItemState(config.clientIDItem);
if (!clientId) return logger.warn("Client ID missing – abort");
const verifier = generateCodeVerifier();
const challenge = generateCodeChallenge(verifier);
items.getItem(config.codeVerifierItem).postUpdate(verifier);
items.getItem(config.codeChallengeItem).postUpdate(challenge);
const payload = [
encodeParam("client_id", clientId),
encodeParam("response_type", "device_code"),
encodeParam("scope", "authenticate_user openid cardata:streaming:read cardata:api:read"),
encodeParam("code_challenge", challenge),
encodeParam("code_challenge_method", "S256")
].join("&");
const headers = new java.util.HashMap();
headers.put("Accept", "application/json");
const url = "https://customer.bmwgroup.com/gcdm/oauth/device/code";
const resp = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, HTTP_TIMEOUT);
const json = JSON.parse(resp);
items.getItem(config.deviceCodeItem).postUpdate(json.device_code);
items.getItem(config.userCodeItem).postUpdate(json.user_code);
items.getItem(config.manualTriggerItem).postUpdate("OFF");
const urlWithCode = json.verification_uri_complete || `${json.verification_uri}?user_code=${json.user_code}`;
sendTelegramSafe(buildTelegramText(urlWithCode, Math.floor((json.expires_in || 600)/60)));
const tg = getTelegramAction();
if (tg) {
tg.sendTelegramQuery("BMW Auth: confirm login", config.bmwAuthenticatieId, "Good", "Failed");
items.getItem(config.telegramReplyId).postUpdate(config.bmwAuthenticatieId);
}
} catch (e) {
logger.error("deviceCodeFlow error: " + e);
}
}
// Poller
function startTokenPolling() {
if (pollingTimer) clearTimeout(pollingTimer);
pollingStartTime = Date.now();
pollingIntervalMs = 5000;
pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
}
function stopTokenPolling() {
if (pollingTimer) clearTimeout(pollingTimer);
pollingTimer = null;
}
function pollForToken() {
if (Date.now() - pollingStartTime > MAX_POLLING_TIME_MS) {
sendTelegramSafe("⏳ BMW Auth: authorization timed out");
stopTokenPolling();
return;
}
try {
dbg("[Poller] Attempting to retrieve token…");
const clientId = getItemState(config.clientIDItem);
const deviceCode = getItemState(config.deviceCodeItem);
const verifier = getItemState(config.codeVerifierItem);
if (!clientId || !deviceCode || !verifier) {
logger.error("[Poller] Missing required data; stopping poller.");
stopTokenPolling();
return;
}
const headers = new java.util.HashMap();
headers.put("Accept", "application/json");
headers.put("User-Agent", "openHAB BMW Token Poller");
const payload = [
encodeParam("client_id", clientId),
encodeParam("device_code", deviceCode),
encodeParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
encodeParam("code_verifier", verifier),
].join("&");
const url = "https://customer.bmwgroup.com/gcdm/oauth/token";
const result = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, HTTP_TIMEOUT);
if (!result) {
logger.error("[Poller] Empty response from BMW during polling; stopping.");
stopTokenPolling();
return;
}
const json = JSON.parse(result);
if (json.error) {
if (json.error === "authorization_pending") {
dbg("[Poller] authorization_pending → retry in " + pollingIntervalMs/1000 + "s");
pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
return;
}
if (json.error === "slow_down") {
pollingIntervalMs = Math.min(pollingIntervalMs + 5000, 15000);
dbg("[Poller] slow_down → new interval " + pollingIntervalMs/1000 + "s");
pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
return;
}
if (json.error === "expired_token") {
logger.error("[Poller] Device code expired — user must authorize again.");
sendTelegramSafe("❌ Authorization expired at BMW. Please restart the login process.");
stopTokenPolling();
return;
}
logger.error("[Poller] BMW Error during poll: " + json.error + " - " + (json.error_description || ""));
stopTokenPolling();
return;
}
if (json.access_token && json.refresh_token) {
dbg("[Poller] >>> TOKEN RECEIVED");
items.getItem(config.tokenAccessItem).postUpdate(json.access_token);
items.getItem(config.tokenRefreshItem).postUpdate(json.refresh_token);
if (json.id_token) {
items.getItem(config.tokenIdItem).postUpdate(json.id_token);
const ok = updateMqttPassword(json.id_token);
const tg = getTelegramAction();
if (ok) {
logger.info("New tokenIdItem written to things file (poller).");
sendTelegramSafe("âś… Tokens retrieved successfully and things file updated.");
} else {
logger.warn("Could not write BMWTokenID to things file (poller).");
sendTelegramSafe("⚠️ Tokens retrieved, but updating the things file failed.");
}
}
const expires = Date.now() + (json.expires_in || 3600) * 1000 - 5*60*1000;
items.getItem(config.refreshTokenExpiryItem).postUpdate(String(expires));
dbg("[Poller] Token received, expires at: " + formatDateLong(expires));
stopTokenPolling();
dbg("[Poller] Poller finished successfully.");
return;
}
logger.error("[Poller] Unexpected poll response (no tokens). Stopping.");
stopTokenPolling();
} catch (e) {
logger.error("[Poller] Exception while polling: " + e + " - " + (e.stack || "no stack"));
stopTokenPolling();
}
}
Good luck