After struggling to find an alternative to the discontinued API used in the bimmer_connected project to poll the battery level of my BMW i3, I took heavy inspiration from the bmw-cardata-home-assitant to integrate the cardata api in Node-RED. I did not create a Node-RED package but am sharing the flows that i use to interact with the API.
This project assumes, you have successfully created a BMW Cardata Account, if you didn't, follow this doc. The Client ID (later called client_id), which is shown after following Step 2 of the doc, is needed later on.
For generating the needed sha256 code_challenge and verifier the crypto module is used, so make sure your settings.js file includes this module:
functionGlobalContext: {
crypto: require("crypto")
}You also need your cars VIN
[{"id":"e1d14bd1d381ad42","type":"comment","z":"8e49a2eb.1c1a3","name":"Cardata","info":"","x":1370,"y":560,"wires":[]},{"id":"125c7a2557e4917a","type":"inject","z":"8e49a2eb.1c1a3","name":"Refresh Token","props":[{"p":"payload"},{"p":"url","v":"https://customer.bmwgroup.com/gcdm/oauth/token","vt":"str"},{"p":"headers","v":"{\"Content-Type\":\"application/x-www-form-urlencoded\"}","vt":"json"},{"p":"method","v":"POST","vt":"str"}],"repeat":"2400","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"grant_type\":\"refresh_token\"}","payloadType":"json","x":1420,"y":820,"wires":[["b9a406ec27382ec0"]]},{"id":"444936b78b25de8c","type":"http request","z":"8e49a2eb.1c1a3","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1970,"y":800,"wires":[["93dbe7353553d452"]]},{"id":"e9fcc17a175357b2","type":"debug","z":"8e49a2eb.1c1a3","name":"Refresh Token","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2420,"y":800,"wires":[]},{"id":"b9a406ec27382ec0","type":"change","z":"8e49a2eb.1c1a3","name":"YOUR_CLIENT_ID","rules":[{"t":"set","p":"payload.refresh_token","pt":"msg","to":"bmw_cardata.refresh_token","tot":"global"},{"t":"set","p":"payload.client_id","pt":"msg","to":"YOUR_CLIENT_ID","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1630,"y":820,"wires":[["c9e9643d79ef500e"]]},{"id":"93dbe7353553d452","type":"switch","z":"8e49a2eb.1c1a3","name":"","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"error","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":2150,"y":800,"wires":[["e9fcc17a175357b2"],["e9fcc17a175357b2","1cba565c5ca94c99"]]},{"id":"66b17ca35f7dad57","type":"debug","z":"8e49a2eb.1c1a3","name":"Authenticate Device","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2360,"y":620,"wires":[]},{"id":"0a1020196ae288af","type":"http request","z":"8e49a2eb.1c1a3","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":2010,"y":640,"wires":[["13a3e3046948255c"]]},{"id":"6d3330fb8ecba2c8","type":"inject","z":"8e49a2eb.1c1a3","name":"Generate Token","props":[{"p":"payload"},{"p":"url","v":"https://customer.bmwgroup.com/gcdm/oauth/token","vt":"str"},{"p":"headers","v":"{\"Content-Type\":\"application/x-www-form-urlencoded\"}","vt":"json"},{"p":"method","v":"POST","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\"}","payloadType":"json","x":1420,"y":740,"wires":[["aac467ac0be4e0d5"]]},{"id":"aac467ac0be4e0d5","type":"change","z":"8e49a2eb.1c1a3","name":"YOUR_CLIENT_ID","rules":[{"t":"set","p":"payload.code_verifier","pt":"msg","to":"bmw_cardata.code_verifier","tot":"global"},{"t":"set","p":"payload.device_code","pt":"msg","to":"bmw_cardata.device_code","tot":"global"},{"t":"set","p":"payload.client_id","pt":"msg","to":"YOUR_CLIENT_ID","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1630,"y":740,"wires":[["4a5f528c45341778"]]},{"id":"fb5a7bfd09e9244f","type":"debug","z":"8e49a2eb.1c1a3","name":"Generate Token","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2160,"y":720,"wires":[]},{"id":"4a5f528c45341778","type":"http request","z":"8e49a2eb.1c1a3","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1830,"y":740,"wires":[["02a36f8eba4d96bd"]]},{"id":"51ddac9a2e4c6078","type":"change","z":"8e49a2eb.1c1a3","name":"set device_code","rules":[{"t":"set","p":"bmw_cardata.device_code","pt":"global","to":"payload.device_code","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2340,"y":660,"wires":[[]]},{"id":"13a3e3046948255c","type":"switch","z":"8e49a2eb.1c1a3","name":"","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"error","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":2170,"y":640,"wires":[["66b17ca35f7dad57"],["51ddac9a2e4c6078","66b17ca35f7dad57"]]},{"id":"02a36f8eba4d96bd","type":"switch","z":"8e49a2eb.1c1a3","name":"","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"error","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1990,"y":740,"wires":[["fb5a7bfd09e9244f"],["fb5a7bfd09e9244f","f6d12277db6a779a"]]},{"id":"f6d12277db6a779a","type":"change","z":"8e49a2eb.1c1a3","name":"","rules":[{"t":"set","p":"bmw_cardata.access_token","pt":"global","to":"payload.access_token","tot":"msg"},{"t":"set","p":"bmw_cardata.refresh_token","pt":"global","to":"payload.refresh_token","tot":"msg"},{"t":"set","p":"bmw_cardata.id_token","pt":"global","to":"payload.id_token","tot":"msg"},{"t":"set","p":"bmw_cardata.gcid","pt":"global","to":"payload.gcid","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload.refresh_token","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2160,"y":760,"wires":[["76f6b5ef01021c13"]]},{"id":"1cba565c5ca94c99","type":"change","z":"8e49a2eb.1c1a3","name":"","rules":[{"t":"set","p":"bmw_cardata.access_token","pt":"global","to":"payload.access_token","tot":"msg"},{"t":"set","p":"bmw_cardata.refresh_token","pt":"global","to":"payload.refresh_token","tot":"msg"},{"t":"set","p":"bmw_cardata.id_token","pt":"global","to":"payload.id_token","tot":"msg"},{"t":"set","p":"bmw_cardata.gcid","pt":"global","to":"payload.gcid","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload.refresh_token","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2420,"y":840,"wires":[["1546b7dd69794a29"]]},{"id":"682abfc36621d66b","type":"inject","z":"8e49a2eb.1c1a3","name":"Create Container","props":[{"p":"payload"},{"p":"url","v":"https://api-cardata.bmwgroup.com/customers/containers","vt":"str"},{"p":"method","v":"POST","vt":"str"},{"p":"headers","v":"{\"Authorization\":\"\",\"accept\":\"application/json\",\"Content-Type\":\"application/json\",\"x-version\":\"v1\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"name\":\"battery\",\"purpose\":\"Get battery info\",\"technicalDescriptors\":[\"vehicle.drivetrain.batteryManagement.header\",\"vehicle.drivetrain.electricEngine.charging.acAmpere\",\"vehicle.drivetrain.electricEngine.charging.acVoltage\",\"vehicle.powertrain.electric.battery.preconditioning.automaticMode.statusFeedback\",\"vehicle.vehicle.avgAuxPower\",\"vehicle.powertrain.tractionBattery.charging.port.anyPosition.flap.isOpen\",\"vehicle.powertrain.tractionBattery.charging.port.anyPosition.isPlugged\",\"vehicle.drivetrain.electricEngine.charging.timeToFullyCharged\",\"vehicle.powertrain.electric.battery.charging.acLimit.selected\",\"vehicle.drivetrain.electricEngine.charging.method\",\"vehicle.body.chargingPort.plugEventId\",\"vehicle.drivetrain.electricEngine.charging.phaseNumber\",\"vehicle.trip.segment.end.drivetrain.batteryManagement.hvSoc\",\"vehicle.trip.segment.accumulated.drivetrain.electricEngine.recuperationTotal\",\"vehicle.drivetrain.electricEngine.remainingElectricRange\",\"vehicle.drivetrain.electricEngine.charging.timeRemaining\",\"vehicle.drivetrain.electricEngine.charging.hvStatus\",\"vehicle.drivetrain.electricEngine.charging.lastChargingReason\",\"vehicle.drivetrain.electricEngine.charging.lastChargingResult\",\"vehicle.powertrain.electric.battery.preconditioning.manualMode.statusFeedback\",\"vehicle.drivetrain.electricEngine.charging.reasonChargingEnd\",\"vehicle.powertrain.electric.battery.stateOfCharge.target\",\"vehicle.body.chargingPort.lockedStatus\",\"vehicle.drivetrain.electricEngine.charging.level\",\"vehicle.powertrain.electric.battery.stateOfHealth.displayed\",\"vehicle.vehicleIdentification.basicVehicleData\",\"vehicle.drivetrain.batteryManagement.batterySizeMax\",\"vehicle.drivetrain.batteryManagement.maxEnergy\",\"vehicle.powertrain.electric.battery.charging.power\",\"vehicle.drivetrain.electricEngine.charging.status\"]}","payloadType":"json","x":1420,"y":920,"wires":[["c5940654bc0e93f3"]]},{"id":"c5940654bc0e93f3","type":"function","z":"8e49a2eb.1c1a3","name":"set access_token","func":"msg.headers.Authorization = \"Bearer \" + global.get(\"bmw_cardata.access_token\");\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1630,"y":920,"wires":[["fea592acfbb30322"]]},{"id":"af635a06e5a58d49","type":"debug","z":"8e49a2eb.1c1a3","name":"Create Container","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":2030,"y":900,"wires":[]},{"id":"fea592acfbb30322","type":"http request","z":"8e49a2eb.1c1a3","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1830,"y":920,"wires":[["af635a06e5a58d49","09abf82ac8814420"]]},{"id":"d8ba8c8af22f6d70","type":"comment","z":"8e49a2eb.1c1a3","name":"Manual authorization via the url and user_code required!","info":"","x":1660,"y":560,"wires":[]},{"id":"9a4129ee11ad6015","type":"inject","z":"8e49a2eb.1c1a3","name":"YOUR VIN","props":[{"p":"url","v":"https://api-cardata.bmwgroup.com/customers/vehicles/YOUR_VIN/telematicData?containerId=","vt":"str"},{"p":"method","v":"GET","vt":"str"},{"p":"headers","v":"{\"Authorization\":\"\",\"accept\":\"application/json\",\"Content-Type\":\"application/json\",\"x-version\":\"v1\"}","vt":"json"}],"repeat":"1800","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1410,"y":1020,"wires":[["f4206780c5a6e0bf"]]},{"id":"f4206780c5a6e0bf","type":"function","z":"8e49a2eb.1c1a3","name":"set url","func":"var containerId = global.get(\"bmw_cardata.containerId\");\n\nif (containerId) {\n msg.url = msg.url + containerId;\n msg.headers.Authorization = \"Bearer \" + global.get(\"bmw_cardata.access_token\");\n return [msg,];\n} else {\n return [,msg];\n}","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1590,"y":1020,"wires":[["d11707a799458a90"],["6e6ba43ca625e115"]]},{"id":"94fa490cf2bbdba9","type":"debug","z":"8e49a2eb.1c1a3","name":"Poll Container","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1940,"y":1000,"wires":[]},{"id":"d11707a799458a90","type":"http request","z":"8e49a2eb.1c1a3","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":1750,"y":1000,"wires":[["94fa490cf2bbdba9"]]},{"id":"df2f4207f7fafdf9","type":"function","z":"8e49a2eb.1c1a3","name":"code gen","func":"// Access the crypto module from global context\nconst crypto = global.get('crypto');\n\n// 1. Generate a random code verifier (length 64, URL-safe)\nfunction base64url(buffer) {\n return buffer.toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n}\n\n// generate 32 random bytes (will become 43+ chars after base64url)\nconst codeVerifier = base64url(crypto.randomBytes(32));\n\n// 2. Generate the S256 code challenge\nconst hash = crypto.createHash('sha256').update(codeVerifier).digest();\nconst codeChallenge = base64url(hash);\n\nglobal.set(\"bmw_cardata.code_verifier\", codeVerifier);\nglobal.set(\"bmw_cardata.code_challenge\", codeChallenge);\n\n// Output both\nmsg.payload.code_challenge = codeChallenge;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1620,"y":640,"wires":[["f7c3a3d257fe2ad3"]]},{"id":"94c7805e696633f1","type":"inject","z":"8e49a2eb.1c1a3","name":"Authenticate Device","props":[{"p":"payload"},{"p":"url","v":"https://customer.bmwgroup.com/gcdm/oauth/device/code","vt":"str"},{"p":"headers","v":"{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Accept\":\"application/json\"}","vt":"json"},{"p":"method","v":"POST","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"response_type\":\"device_code\",\"scope\":\"authenticate_user openid cardata:streaming:read cardata:api:read\",\"code_challenge_method\":\"S256\"}","payloadType":"json","x":1430,"y":640,"wires":[["df2f4207f7fafdf9"]]},{"id":"f7c3a3d257fe2ad3","type":"change","z":"8e49a2eb.1c1a3","name":"YOUR_CLIENT_ID","rules":[{"t":"set","p":"payload.code_challenge","pt":"msg","to":"bmw_cardata.code_challenge","tot":"global"},{"t":"set","p":"payload.client_id","pt":"msg","to":"YOUR_CLIENT_ID","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1810,"y":640,"wires":[["0a1020196ae288af"]]},{"id":"76f6b5ef01021c13","type":"file","z":"8e49a2eb.1c1a3","name":"","filename":"./refresh_token","filenameType":"str","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":2360,"y":760,"wires":[[]]},{"id":"1546b7dd69794a29","type":"file","z":"8e49a2eb.1c1a3","name":"","filename":"./refresh_token","filenameType":"str","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":2620,"y":840,"wires":[[]]},{"id":"c9e9643d79ef500e","type":"switch","z":"8e49a2eb.1c1a3","name":"","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"refresh_token","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1810,"y":820,"wires":[["444936b78b25de8c"],["03174010ba262c25"]]},{"id":"03174010ba262c25","type":"file in","z":"8e49a2eb.1c1a3","name":"","filename":"./refresh_token","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":1980,"y":840,"wires":[["2a3362c04cdf43eb"]]},{"id":"2a3362c04cdf43eb","type":"function","z":"8e49a2eb.1c1a3","name":"get/set refresh_token","func":"global.set(\"bmw_cardata.refresh_token\", msg.payload);\nmsg.payload = {\"grant_type\":\"refresh_token\"};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2200,"y":840,"wires":[["b9a406ec27382ec0"]]},{"id":"6e6ba43ca625e115","type":"file in","z":"8e49a2eb.1c1a3","name":"","filename":"./containerId","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":1750,"y":1040,"wires":[["4ce2e25f7d8aab7b"]]},{"id":"4ce2e25f7d8aab7b","type":"function","z":"8e49a2eb.1c1a3","name":"set containerId","func":"global.set(\"bmw_cardata.containerId\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1940,"y":1040,"wires":[["f4206780c5a6e0bf"]]},{"id":"09abf82ac8814420","type":"change","z":"8e49a2eb.1c1a3","name":"","rules":[{"t":"set","p":"bmw_cardata.containerId","pt":"global","to":"payload.containerId","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":2080,"y":940,"wires":[["4c39381631f8950c"]]},{"id":"4c39381631f8950c","type":"file","z":"8e49a2eb.1c1a3","name":"","filename":"./containerId","filenameType":"str","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":2330,"y":940,"wires":[[]]}]To import the flows copy the json data above or from the flows.json file, click on the hamburger menu in the top right of Node-RED and select Import.
After importing the flows add the earlier copied client_id in the 3 change nodes that are called YOUR CLIENT_ID and paste in your cars VIN in the YOUR VIN Inject (msg.url field).
Hit the inject once, it returns a bunch of stuff, we focus on the user_code which is needed to manually authenticate the access. Visit the url shown in the debug panel, log in and paste the user_code in the text input.
Use the Generate Token Inject to generate your first access token.
You do not need to use the Refresh Token Inject manually, it is set to run every 40 minutes.
Trigger the Create Container inject to create a container via the API's POST endpoint.
After creating your container you can start pulling data from the API, use the YOUR VIN Inject to poll the API with the container you created before, this is also set to run every 30 minutes. Pay attention to the daily API rate limit of 50 polls per day!
These flows were exported from Node-RED version 4.0.5 so look out for any incompatibility when using a different version.
You need to create a container in which you set the telemetry data keys that are going to be returned everytime you poll the API. This is done through the earlier mentioned Create Container Inject. Feel free to edit the value of the msg.payload.technicalDescriptors key in this inject to adjust the telemetric data keys that are getting polled. The possible keys are found in this catalogue, just turn on the Expanded View toggle to see the actual Technical identifiers.
I Implemented the functionality to save and later on restore the refresh token and containerId in local files respectively in case they are not found in the global variable e.g. after a restart of Node-RED. So this project stores access tokens locally, be aware of this in case you do not want it! To remove this functionality remove the ./refresh_token and ./containerId file write nodes.
A big thanks to the developers of the bmw-cardata-home-assitant project, without them i wouldn't have been able to get my flows working