Skip to content

Floplosion05/node-red-bmw-cardata

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 

Repository files navigation

node-red-bmw-cardata

Introduction

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.

Prerequisites

Cardata

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.

Node-RED settings file (settings.js)

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")
}

Car

You also need your cars VIN

Node-RED importing flows

[{"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.

Usage

  1. Adding Client ID and VIN

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.

  1. Generate Token

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.

  1. Polling Container

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!

Incompatibility

These flows were exported from Node-RED version 4.0.5 so look out for any incompatibility when using a different version.

Create Container

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.

Security Disclaimer

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.

Credits

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

About

NodeRed flows to interact with the BMW Cardata API (token generation & renewal, container creation and polling)

Topics

Resources

License

Stars

3 stars

Watchers

3 watching

Forks

Contributors