Binding for BMW i3

Hello,
searching a way to get battery status and temperature out of my i3 direct into
openhab slight_smile:

i found this BMW i3 API
https://github.com/edent/BMW-i-Remote/

I`m not good enough to get this Api to work and show the data in Openhab.
Perhaps can anybody give me help to get this binding to work…

Thanks

This API isn’t the same as a binding! For a direct integration in OH a binding could be used. But this has to be developped by anyone.

But with the informations you have linked you can make your own http requests (using the existing http binding) to use the data in OH. But according to the readme you have to provide some informations in advance:


You can get the i Remote details from either decompiling the Android App or from intercepting communications between your phone and the BMW server. This is left as an exercise for the reader :slight_smile:


So this seems not to be trivial…

If you are able to get past the hurdle that @jaydee73 correctly highlights, you could then use the Python examples, modified as needed, and use the Exec binding to run the Python code repeatedly to update items. Otherwise a proper binding that handles the OAuth negotiation would be the cleanest. Have a look at the Tesla binding as a possible model for a new BMW i3 binding.

Getting the API key and the secret is easy if you do it as described here.
I was successfull with the recommended “packet capture by grey shirts” android app. The good thing is that it doesn’t even need root access, 10 mins and you’re done.

I also thought about a binding for the I3, but didn’t found the time so far (besides that I’m not a professional dev)

I manged to get my bmw to openhab2 here is my solution:

bmw.items

String	doorLockState  		"BMW [%s]"
String	doorDriverFront  	"Fahrertür [%s]"
String	doorPassengerFront	"Beifahrertür [%s]"
String	doorDriverRear  	"Hintenfahrertür [%s]"
String	doorPassengerRear  	"Hintenbeifahrertür [%s]"
Number	chargingLevelHv 	"Ladetestatus [%d Prz]"
Number	remainingRangeElectric 	"Reichweite [%d Km]"

Switch bmwforceupadte {}
String access_token "AccessToken [%s]"
String token_expiry "TokenExpiry [%s]"
String bmwusername "Username [yourusername]"
String bmwpassword "Username [yourpassword]"
String bmwauth_basic "AuthBasic [yourAuthBasic]"

bmw.rules

rule "BMW"
when
		Time cron "0 0/30 * * * ?"
	then		
			val bmwresp =  executeCommandLine("/usr/bin/python /etc/openhab2/scripts/bmw.py", 5000)
			logInfo("BMW", bmwresp)		
end

rule "BMW run script"
when
		Item bmwforceupadte received update
	then		
			val bmwresp =  executeCommandLine("/usr/bin/python /etc/openhab2/scripts/bmw.py", 5000)
			logInfo("BMW", bmwresp)
end

bmw.py just save it to the openhab config scripts path

#! /usr/bin/env python
#
# Use the BMW ConnectedDrive API using credentials from credentials.json
# You can see what should be in there by looking at credentials.json.sample.
#
# 'auth_basic' is the base64-encoded version of API key:API secret
# You can capture it if you can intercept the traffic from the app at
# the time when reauthentication is happening.
#
# Based on the excellent work by Terence Eden:
# https://github.com/edent/BMW-i-Remote

import json
import requests
import time


#   API Gateway
ROOT_URL     = "https://b2vapi.bmwgroup.com/webapi"
API_ROOT_URL = ROOT_URL + '/v1'
OpenHabIP = "192.168.1.37:8080"

# What are we pretending to be? Not sure if this is important.
# Might be tied to OAuth consumer (auth_basic) credentials?
USER_AGENT = "MCVApp/1.5.2 (iPhone; iOS 9.1; Scale/2.00)"
# USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LMY48Y)"

# Constants
#   To convert km to miles:
#   miles = km * KM_TO_MILES
KM_TO_MILES  = 0.621371
#   To convert kWh/100Km to Miles/kWh:
#   1 / (EFFICIENCY * avgElectricConsumption)`
EFFICIENCY = 0.01609344

# For future use
class ConnectedDriveException(Exception):
    pass

class ConnectedDrive(object):
    """
    A wrapper for the BMW ConnectedDrive API used by mobile apps.

    Caches credentials in credentials_file, so needs both read
    and write access to it.
    """
    def __init__(self):
		

        username2 = json.loads(str(requests.get('http://' + str(OpenHabIP) + '/rest/items/bmwusername').content))
        self.username = username2["stateDescription"]["pattern"]
        password2 = json.loads(str(requests.get('http://' + str(OpenHabIP) + '/rest/items/bmwpassword').content))
        self.password = password2["stateDescription"]["pattern"]
        auth_basic2 = json.loads(str(requests.get('http://' + str(OpenHabIP) + '/rest/items/bmwauth_basic').content))
        self.auth_basic = auth_basic2["stateDescription"]["pattern"]
        self.access_token = str(requests.get('http://' + str(OpenHabIP) + '/rest/items/access_token/state').content)
        if(self.access_token == "NULL"):
            requests.put('http://' + str(OpenHabIP) + '/rest/items/access_token/state', "")
        self.token_expiry =  str(requests.get('http://' + str(OpenHabIP) + '/rest/items/token_expiry/state').content)
        if(self.token_expiry == "NULL"):
			self.token_expiry = 1495039940.767906
        if (time.time() > float(self.token_expiry)):
            self.generateCredentials()
      
    def generateCredentials(self):
        """
        If previous token has expired, create a new one from basics.
        """
        headers = {
            "Authorization": "Basic " + self.auth_basic, 
            "Content-Type": "application/x-www-form-urlencoded",
            "User-Agent": USER_AGENT
        }

        data = { 
            "grant_type": "password",
            "username": self.username, 
            "password": self.password,
            "scope": "remote_services vehicle_data"
        }

        r = requests.post(ROOT_URL + "/oauth/token/",  data=data, headers=headers)
        json_data = r.json()
        
        #   Get the access token
        self.access_token = json_data["access_token"]
        self.token_expiry = time.time() + json_data["expires_in"]
        self.saveCredentials()


    def saveCredentials(self):
    	"""
    	Save current state to the JSON file.
    	"""
        credentials = {
            "username": self.username,
            "password": self.password,
            "auth_basic": self.auth_basic,
            "access_token": self.access_token,
            "token_expiry": self.token_expiry
        }
        requests.put('http://' + str(OpenHabIP) + '/rest/items/access_token/state', str(self.access_token))
        requests.put('http://' + str(OpenHabIP) + '/rest/items/token_expiry/state', str(self.token_expiry))
                                  


    def call(self, path, post_data=None):
        """
        Call the API at the given path.

        Argument should be relative to the API base URL, e.g:
        
            print c.call('/user/vehicles/')

        If a dictionary 'post_data' is specified, the request will be
        a POST, otherwise a GET.
        """
        # 
        if (time.time() > self.token_expiry):
            self.generateCredentials()

        headers = {"Authorization": "Bearer " + self.access_token, 
                   "User-Agent":USER_AGENT}

        if post_data is None:
            r = requests.get(API_ROOT_URL + path,  headers=headers)
        else:
            r = requests.post(API_ROOT_URL + path,  headers=headers, data=post_data)
        return r.json()


    def executeService(self, vin, serviceType):
        """
        Post a request for the specified service. e.g.

            print c.executeService(vin, 'DOOR_LOCK')

        """
        return self.call("/user/vehicles/{}/executeService".format(vin),
            {'serviceType': serviceType})


# A simple test example
def main():
    c = ConnectedDrive()

    #print "\nVehicle info"
    resp = c.call('/user/vehicles/')
    car = resp['vehicles'][0]
    #for k,v in car.items():
        #print "  ",k, " : ", v

    print "\nVehicle status"
    status = c.call("/user/vehicles/{}/status".format(car['vin']))['vehicleStatus']
    for k,v in status.items():
        print "  ", k, " : ", v
    print "\nAusgabe"
    #position = status['position'];
    #for k,v in position.items():
        #print "  ", k, " : ", v
    #print position['lat']

    requests.put('http://' + str(OpenHabIP) + '/rest/items/chargingLevelHv/state', str(status['chargingLevelHv']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/doorLockState/state',  str(status['doorLockState']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/doorDriverFront/state', str(status['doorDriverFront']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/doorPassengerFront/state', str(status['doorPassengerFront']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/doorDriverRear/state', str(status['doorDriverRear']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/doorPassengerRear/state', str(status['doorPassengerRear']))
    requests.put('http://' + str(OpenHabIP) + '/rest/items/remainingRangeElectric/state', str(status['remainingRangeElectric']))

if __name__ == '__main__':
    main()

Sitemap

		 Text label="BMW" icon="bmw1600" {
		 	Text item=chargingLevelHv valuecolor=[>90="green",>45="orange",<=45="red"] icon="battery-100"
		 	Text item=remainingRangeElectric icon="line-incline"
		 	Text item=doorLockState {
				Text item=doorDriverFront
				Text item=doorPassengerFront
				Text item=doorDriverRear
				Text item=doorPassengerRear
				}		
			Switch item=bmwforceupadte
			Text item=Connect {
				Text item=access_token
				Text item=token_expiry
				Text item=bmwusername
				Text item=bmwpassword
				Text item=bmwauth_basic
			}
		 }

This looks really nice! While my i3 will not arrive until later on this summer, I can see that there’s one thing missing - “Start Climate Control”. Apart from this it looks great!

@Nils81: great, will try it out on weekend

I was able to successfully retrieve data from the car without having to get the secret keys (which was impossible for me due to the certificate pinning in place). I ended up using the same APIs from the Connected Drive web site and using their OAuth to authenticate. Seems to work stable now for a few weeks.

Here’s how it looks on HabPanel:

So, I finally got my car, and are ready to add it to OH.

But I am probably thick - I don’t understand where to find the API key and secret, or - I’m missing what they are.

I can get the data from packet capture, and I see the Authorisation: Bearer etc etc (exactly as exampled on Terence Eden’s blog), But What is the API key? And what is the secret?

Or, perhaps the other route it better, since I think that the mobile route means that we have to supply position (to be close to the car). But is there a write up how to do it the oauth way? OAuth seams more complicated?

Edit:
OK, so I read a bit more into detail - the api key and secret can be derived from the base64 “Authorization: Basic” found in the login sequence.

@pmpkk Could you maybe share more details? I want to do the same right now but I’m stuck because the oauth/authenticate gives me a 401

{
“error”: “invalid_client”,
“error_description”: “Client authentication failed (unknown client)”
}

although I added the client_id header which I could pick up from the form headers.

@Lyve If it is still relevant for you, here is a working example for accessing BMW connectedDrive via PHP.

@pmpkk Would you mind to share your python code?

I have migrated parts of the php-code from Sergej Mueller to python.
No more need for the api key and secret, only ConnectedDrive user / password and the VIN of the car.
I’ll upload the code to github (probably on the weekend)

1 Like

I have uploaded the script, see here:
Script to access the BMW ConnectedDrive portal via OH