From 5338f59ad6e9a33d850df1a1713b0a09f4053f0e Mon Sep 17 00:00:00 2001 From: rleidner Date: Fri, 26 Apr 2024 21:27:36 +0200 Subject: [PATCH 1/4] BMW-SOC-Module: fix authentication --- modules/soc_i3/i3soc.py | 178 +++++++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 58 deletions(-) diff --git a/modules/soc_i3/i3soc.py b/modules/soc_i3/i3soc.py index d6b8782cd..e8e023657 100755 --- a/modules/soc_i3/i3soc.py +++ b/modules/soc_i3/i3soc.py @@ -4,16 +4,16 @@ import requests import string import sys -import time +# import time import urllib +import uuid +import hashlib + # ---------------Constants------------------------------------------- auth_server = 'customer.bmwgroup.com' api_server = 'cocoapi.bmwgroup.com' -client_id = '31c357a0-7a1d-4590-aa99-33b97244d048' -client_password = 'c0e3393d-70a2-4f6f-9d3c-8530af64d552' - # ---------------Helper Function------------------------------------------- def get_random_string(length: int) -> str: @@ -30,6 +30,12 @@ def create_auth_string(client_id: str, client_password: str) -> str: return authstring +def create_s256_code_challenge(code_verifier: str) -> str: + """Create S256 code_challenge with the given code_verifier.""" + data = hashlib.sha256(code_verifier.encode("ascii")).digest() + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("UTF-8") + + # ---------------HTTP Function------------------------------------------- def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = 30) -> str: try: @@ -37,8 +43,8 @@ def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = except requests.Timeout: print("Connection Timeout") raise - except: - print("HTTP Error") + except Exception as e: + print("HTTP Error:" + str(e)) raise if response.status_code == 200 or response.status_code == 204: @@ -48,10 +54,17 @@ def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = raise RuntimeError -def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '', timeout: int = 30, allow_redirects: bool = True) -> str: +def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '', + timeout: int = 30, allow_redirects: bool = True, + authId: str = '', authSec: str = '') -> str: try: - response = requests.post(url, data=data, headers=headers, cookies=cookies, - timeout=timeout, allow_redirects=allow_redirects) + if authId != '': + response = requests.post(url, data=data, headers=headers, cookies=cookies, + timeout=timeout, auth=(authId, authSec), + allow_redirects=allow_redirects) + else: + response = requests.post(url, data=data, headers=headers, cookies=cookies, + timeout=timeout, allow_redirects=allow_redirects) except requests.Timeout: print("Connection Timeout") raise @@ -68,96 +81,145 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' raise RuntimeError +def authStage0(region: str, username: str, password: str) -> str: + try: + id0 = str(uuid.uuid4()) + id1 = str(uuid.uuid4()) + apiKey = b'NGYxYzg1YTMtNzU4Zi1hMzdkLWJiYjYtZjg3MDQ0OTRhY2Zh' + ocp = base64.b64decode(apiKey).decode() + url = 'https://' + api_server + '/eadrax-ucs/v1/presentation/oauth/config' + headers = { + 'ocp-apim-subscription-key': ocp, + 'bmw-session-id': id0, + 'x-identity-provider': 'gcdm', + 'x-correlation-id': id1, + 'bmw-correlation-Id': id1, + 'user-agent': 'Dart/3.0 (dart:io)', + 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + body = getHTTP(url, headers) + cfg = json.loads(body) + except: + print("authStage0 failed") + raise + return cfg + + # ---------------Authentication Function------------------------------------------- -def authStage1(username: str, password: str, code_challenge: str, state: str) -> str: +def authStage1(url: str, + username: str, + password: str, + code_challenge: str, + state: str, + nonce: str) -> str: + global config try: - url = 'https://' + auth_server + '/gcdm/oauth/authenticate' + # url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'} + 'user-agent': 'Dart/3.0 (dart:io)', + 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} data = { - 'client_id': client_id, + 'client_id': config['clientId'], 'response_type': 'code', - 'scope': 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', - 'redirect_uri': 'com.bmw.connected://oauth', + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], 'state': state, - 'nonce': 'login_nonce', + 'nonce': nonce, 'code_challenge': code_challenge, - 'code_challenge_method': 'plain', + 'code_challenge_method': 'S256', 'username': username, 'password': password, 'grant_type': 'authorization_code'} - - response = json.loads(postHTTP(url, data, headers)) + + resp = postHTTP(url, data, headers) + response = json.loads(resp) authcode = dict(urllib.parse.parse_qsl(response["redirect_to"]))["authorization"] + # print("authStage1: authcode=" + authcode) except: print("Authentication stage 1 failed") raise - + return authcode -def authStage2(authcode1: str, code_challenge: str, state: str) -> str: +def authStage2(url: str, authcode1: str, code_challenge: str, state: str, nonce: str) -> str: try: - url = 'https://' + auth_server + '/gcdm/oauth/authenticate' + # url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'} + 'user-agent': 'Dart/3.0 (dart:io)', + 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} data = { - 'client_id': client_id, + 'client_id': config['clientId'], 'response_type': 'code', - 'scope': 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', - 'redirect_uri': 'com.bmw.connected://oauth', + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], 'state': state, - 'nonce': 'login_nonce', + 'nonce': nonce, 'code_challenge': code_challenge, - 'code_challenge_method': 'plain', + 'code_challenge_method': 'S256', 'authorization': authcode1} cookies = { 'GCDMSSO': authcode1} - + response = postHTTP(url, data, headers, cookies, allow_redirects=False) - authcode = dict(urllib.parse.parse_qsl(response.split("?",1)[1]))["code"] + # print("authStage2: response=" + response) + authcode = dict(urllib.parse.parse_qsl(response.split("?", 1)[1]))["code"] + # print("authStage2: authcode=" + authcode) except: print("Authentication stage 2 failed") raise - + return authcode -def authStage3(authcode2: str, code_challenge: str) -> dict: +def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: + global config try: - url = 'https://' + auth_server + '/gcdm/oauth/token' + url = token_url headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Authorization': create_auth_string(client_id, client_password)} + 'Authorization': (config['clientId'], config['clientSecret'])} + headers = { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} data = { 'code': authcode2, - 'code_verifier': code_challenge, - 'redirect_uri': 'com.bmw.connected://oauth', + 'code_verifier': code_verifier, + 'redirect_uri': config['returnUrl'], 'grant_type': 'authorization_code'} - - response = postHTTP(url, data, headers, allow_redirects=False) + authId = config['clientId'] + authSec = config['clientSecret'] + response = postHTTP(url, data, headers, authId=authId, authSec=authSec, allow_redirects=False) + # print("authStage3: response=" + response) token = json.loads(response) + # print("authStage3: token=" + json.dumps(token, indent=4)) except: print("Authentication stage 3 failed") raise - + return token def requestToken(username: str, password: str) -> dict: + global config try: - code_challenge = get_random_string(86) + # new: get oauth config from server + config = authStage0('0', username, password) + # print('config=\n' + json.dumps(config, indent=4)) + token_url = config['tokenEndpoint'] + authenticate_url = token_url.replace('/token', '/authenticate') + code_verifier = get_random_string(86) + code_challenge = create_s256_code_challenge(code_verifier) state = get_random_string(22) - - authcode1 = authStage1(username, password, code_challenge, state) - authcode2 = authStage2(authcode1, code_challenge, state) - token = authStage3(authcode2, code_challenge) + nonce = get_random_string(22) + + authcode1 = authStage1(authenticate_url, username, password, code_challenge, state, nonce) + authcode2 = authStage2(authenticate_url, authcode1, code_challenge, state, nonce) + token = authStage3(token_url, authcode2, code_verifier) except: print("Login failed") raise - + return token @@ -171,19 +233,19 @@ def requestData(token: str, vin: str) -> dict: else: print("Unknown VIN") raise RuntimeError - + url = 'https://' + api_server + '/eadrax-vcs/v4/vehicles/state' headers = { - 'User-Agent': 'Dart/2.14 (dart:io)', - 'x-user-agent': 'android(SP1A.210812.016.C1);' + brand + ';2.5.2(14945);row', + 'user-agent': 'Dart/3.0 (dart:io)', + 'x-user-agent': 'android(TQ2A.230405.003.B2);' + brand + ';3.11.1(29513);0', 'bmw-vin': vin, 'Authorization': (token["token_type"] + " " + token["access_token"])} - body = getHTTP(url, headers) + body = getHTTP(url, headers) response = json.loads(body) except: print("Data-Request failed") - raise - + raise + return response @@ -192,7 +254,7 @@ def main(): try: argsStr = base64.b64decode(str(sys.argv[1])).decode('utf-8') argsDict = json.loads(argsStr) - + username = str(argsDict["user"]) password = str(argsDict["pass"]) vin = str(argsDict["vin"]).upper() @@ -202,7 +264,7 @@ def main(): except: print("Parameters could not be processed") raise - + try: token = requestToken(username, password) data = requestData(token, vin) @@ -210,9 +272,9 @@ def main(): print("Download sucessful - SoC: " + str(soc) + "%") except: print("Request failed") - raise - - try: + raise + + try: with open(socfile, 'w') as f: f.write(str(int(soc))) state = {} @@ -220,10 +282,10 @@ def main(): with open(meterfile, 'r') as f: state["meter"] = float(f.read()) with open(statefile, 'w') as f: - f.write(json.dumps(state)) + f.write(json.dumps(state)) except: print("Saving SoC failed") - raise + raise if __name__ == '__main__': From 4556055e586fc32eaee6bf676b9b05fc459a8696 Mon Sep 17 00:00:00 2001 From: rleidner Date: Sat, 27 Apr 2024 17:20:58 +0200 Subject: [PATCH 2/4] BMW SOC: add token refresh, improve logging --- modules/soc_i3/i3soc.py | 227 ++++++++++++++++++++++++++++++++++------ 1 file changed, 197 insertions(+), 30 deletions(-) diff --git a/modules/soc_i3/i3soc.py b/modules/soc_i3/i3soc.py index e8e023657..2eae51a5c 100755 --- a/modules/soc_i3/i3soc.py +++ b/modules/soc_i3/i3soc.py @@ -4,7 +4,8 @@ import requests import string import sys -# import time +import os +import time import urllib import uuid import hashlib @@ -13,9 +14,35 @@ # ---------------Constants------------------------------------------- auth_server = 'customer.bmwgroup.com' api_server = 'cocoapi.bmwgroup.com' +REGION = '0' # rest_of_world +storeFile = 'i3soc.json' + + +# --------------- Global Variables -------------------------------------- +store = {} +config = {} +DEBUGLEVEL = 0 +method = '' # ---------------Helper Function------------------------------------------- + +def _error(txt: str): + print(txt) + + +def _info(txt: str): + global DEBUGLEVEL + if DEBUGLEVEL >= 1: + print(txt) + + +def _debug(txt: str): + global DEBUGLEVEL + if DEBUGLEVEL > 1: + print(txt) + + def get_random_string(length: int) -> str: letters = string.ascii_letters result_str = ''.join(random.choice(letters) for i in range(length)) @@ -36,21 +63,68 @@ def create_s256_code_challenge(code_verifier: str) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode("UTF-8") +# initialize store structures when no store is available +def init_store(): + global store + store = {} + store['Token'] = {} + store['expires_at'] = int(0) + + +# load store from file, initialize store structure if no file exists +def load_store(): + global store + global storeFile + try: + tf = open(storeFile, 'r', encoding='utf-8') + store = json.load(tf) + if 'Token' not in store: + init_store() + tf.close() + except FileNotFoundError: + _error("load_store: store file not found, new authentication required") + store = {} + init_store() + except Exception as e: + _error("init: loading stored data failed, file: " + + storeFile + ", error=" + str(e)) + store = {} + init_store() + + +# write store file +def write_store(): + global store + global storeFile + try: + tf = open(storeFile, 'w', encoding='utf-8') + except Exception as e: + _error("write_store_file: Exception " + str(e)) + os.system("sudo rm -f " + storeFile) + tf = open(storeFile, 'w', encoding='utf-8') + json.dump(store, tf, indent=4) + tf.close() + try: + os.chmod(storeFile, 0o777) + except Exception as e: + os.system("sudo chmod 0777 " + storeFile) + + # ---------------HTTP Function------------------------------------------- def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = 30) -> str: try: response = requests.get(url, headers=headers, cookies=cookies, timeout=timeout) except requests.Timeout: - print("Connection Timeout") + _error("Connection Timeout") raise except Exception as e: - print("HTTP Error:" + str(e)) + _error("HTTP Error:" + str(e)) raise if response.status_code == 200 or response.status_code == 204: return response.text else: - print('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, StatusCode: ' + str(response.status_code)) raise RuntimeError @@ -58,6 +132,9 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' timeout: int = 30, allow_redirects: bool = True, authId: str = '', authSec: str = '') -> str: try: + _debug("postHTTP: url=" + url + + ",\nheaders=" + json.dumps(headers, indent=4) + + ",\ndata=" + json.dumps(data, indent=4)) if authId != '': response = requests.post(url, data=data, headers=headers, cookies=cookies, timeout=timeout, auth=(authId, authSec), @@ -66,10 +143,10 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' response = requests.post(url, data=data, headers=headers, cookies=cookies, timeout=timeout, allow_redirects=allow_redirects) except requests.Timeout: - print("Connection Timeout") + _error("Connection Timeout") raise except: - print("HTTP Error") + _error("HTTP Error") raise if response.status_code == 200 or response.status_code == 204: @@ -77,11 +154,11 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' elif response.status_code == 302: return response.headers["location"] else: - print('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, StatusCode: ' + str(response.status_code)) raise RuntimeError -def authStage0(region: str, username: str, password: str) -> str: +def authStage0(region: str) -> str: try: id0 = str(uuid.uuid4()) id1 = str(uuid.uuid4()) @@ -95,11 +172,11 @@ def authStage0(region: str, username: str, password: str) -> str: 'x-correlation-id': id1, 'bmw-correlation-Id': id1, 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);' + region} body = getHTTP(url, headers) cfg = json.loads(body) except: - print("authStage0 failed") + _error("authStage0 failed") raise return cfg @@ -113,7 +190,6 @@ def authStage1(url: str, nonce: str) -> str: global config try: - # url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'user-agent': 'Dart/3.0 (dart:io)', @@ -134,9 +210,9 @@ def authStage1(url: str, resp = postHTTP(url, data, headers) response = json.loads(resp) authcode = dict(urllib.parse.parse_qsl(response["redirect_to"]))["authorization"] - # print("authStage1: authcode=" + authcode) + _debug("authStage1: authcode=" + authcode) except: - print("Authentication stage 1 failed") + _error("Authentication stage 1 failed") raise return authcode @@ -144,7 +220,6 @@ def authStage1(url: str, def authStage2(url: str, authcode1: str, code_challenge: str, state: str, nonce: str) -> str: try: - # url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'user-agent': 'Dart/3.0 (dart:io)', @@ -163,11 +238,11 @@ def authStage2(url: str, authcode1: str, code_challenge: str, state: str, nonce: 'GCDMSSO': authcode1} response = postHTTP(url, data, headers, cookies, allow_redirects=False) - # print("authStage2: response=" + response) + _debug("authStage2: response=" + response) authcode = dict(urllib.parse.parse_qsl(response.split("?", 1)[1]))["code"] - # print("authStage2: authcode=" + authcode) + _debug("authStage2: authcode=" + authcode) except: - print("Authentication stage 2 failed") + _error("Authentication stage 2 failed") raise return authcode @@ -190,11 +265,11 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: authId = config['clientId'] authSec = config['clientSecret'] response = postHTTP(url, data, headers, authId=authId, authSec=authSec, allow_redirects=False) - # print("authStage3: response=" + response) + _debug("authStage3: response=" + response) token = json.loads(response) - # print("authStage3: token=" + json.dumps(token, indent=4)) + _debug("authStage3: token=" + json.dumps(token, indent=4)) except: - print("Authentication stage 3 failed") + _error("Authentication stage 3 failed") raise return token @@ -202,10 +277,12 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: def requestToken(username: str, password: str) -> dict: global config + global method try: # new: get oauth config from server - config = authStage0('0', username, password) - # print('config=\n' + json.dumps(config, indent=4)) + method += ' requestToken' + config = authStage0(REGION) + _debug('config=\n' + json.dumps(config, indent=4)) token_url = config['tokenEndpoint'] authenticate_url = token_url.replace('/token', '/authenticate') code_verifier = get_random_string(86) @@ -217,7 +294,34 @@ def requestToken(username: str, password: str) -> dict: authcode2 = authStage2(authenticate_url, authcode1, code_challenge, state, nonce) token = authStage3(token_url, authcode2, code_verifier) except: - print("Login failed") + _error("Login failed") + raise + + return token + + +def refreshToken(refreshToken: str) -> dict: + global config + global method + try: + method += ' refreshToken' + config = authStage0(REGION) + url = config['tokenEndpoint'] + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'user-agent': 'Dart/3.0 (dart:io)', + 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + data = { + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], + 'grant_type': 'refresh_token', + 'refresh_token': refreshToken} + authId = config['clientId'] + authSec = config['clientSecret'] + resp = postHTTP(url, data, headers, authId=authId, authSec=authSec, allow_redirects=False) + token = json.loads(resp) + except: + _error("Login failed") raise return token @@ -225,13 +329,15 @@ def requestToken(username: str, password: str) -> dict: # ---------------Interface Function------------------------------------------- def requestData(token: str, vin: str) -> dict: + global method try: + method += ' requestData' if vin[:2] == 'WB': brand = 'bmw' elif vin[:2] == 'WM': brand = 'mini' else: - print("Unknown VIN") + _error("Unknown VIN") raise RuntimeError url = 'https://' + api_server + '/eadrax-vcs/v4/vehicles/state' @@ -243,7 +349,9 @@ def requestData(token: str, vin: str) -> dict: body = getHTTP(url, headers) response = json.loads(body) except: - print("Data-Request failed") + _error("Data-Request failed") + _error("requestData: url=" + url + + ",\nheaders=" + json.dumps(headers, indent=4)) raise return response @@ -251,7 +359,18 @@ def requestData(token: str, vin: str) -> dict: # ---------------Main Function------------------------------------------- def main(): + global store + global storeFile + global DEBUGLEVEL + global method try: + method = '' + CHARGEPOINT = os.environ.get("CHARGEPOINT", "1") + DEBUGLEVEL = int(os.environ.get("debug", "0")) + RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined") + storeFile = RAMDISKDIR + '/soc_i3_cp' + CHARGEPOINT + '.json' + _debug('storeFile =' + storeFile) + argsStr = base64.b64decode(str(sys.argv[1])).decode('utf-8') argsDict = json.loads(argsStr) @@ -262,16 +381,64 @@ def main(): meterfile = str(argsDict["meterfile"]) statefile = str(argsDict["statefile"]) except: - print("Parameters could not be processed") + _error("Parameters could not be processed") raise try: - token = requestToken(username, password) + # try to read store file from ramdisk + expires_in = -1 + load_store() + now = int(time.time()) + _debug('main0: store=\n' + json.dumps(store, indent=4)) + # if OK, check if refreshToken is required + if 'expires_at' in store and \ + 'Token' in store and \ + 'expires_in' in store['Token'] and \ + 'refresh_token' in store['Token']: + expires_in = store['Token']['expires_in'] + expires_at = store['expires_at'] + token = store['Token'] + _debug('main0: expires_in=' + str(expires_in) + ', now=' + str(now) + + ', expires_at=' + str(expires_at) + ', diff=' + str(expires_at - now)) + if now > expires_at - 120: + _debug('call refreshToken') + token = refreshToken(token['refresh_token']) + if 'expires_in' in token: + expires_in = int(token['expires_in']) + expires_at = now + expires_in + store['expires_at'] = expires_at + store['Token'] = token + write_store() + else: + _error("refreshToken failed, re-authenticate") + expires_in = -1 + else: + expires_in = store['Token']['expires_in'] + + # if refreshToken fails, call requestToken + if expires_in == -1: + _debug('call requestToken') + token = requestToken(username, password) + + # compute expires_at and store file in ramdisk + if 'expires_in' in token: + if expires_in != int(token['expires_in']): + expires_in = int(token['expires_in']) + expires_at = now + expires_in + store['expires_at'] = expires_at + store['Token'] = token + write_store() + else: + _error("requestToken failed") + store['expires_at'] = 0 + store['Token'] = token + write_store() + _debug('main: token=\n' + json.dumps(token, indent=4)) data = requestData(token, vin) soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"]) - print("Download sucessful - SoC: " + str(soc) + "%") + _info("Successful - SoC: " + str(soc) + "%" + ', method=' + method) except: - print("Request failed") + _error("Request failed") raise try: @@ -284,7 +451,7 @@ def main(): with open(statefile, 'w') as f: f.write(json.dumps(state)) except: - print("Saving SoC failed") + _error("Saving SoC failed") raise From 5122938ed8250438628c1a29b6d1b617b7ab57df Mon Sep 17 00:00:00 2001 From: rleidner Date: Sun, 28 Apr 2024 23:21:24 +0200 Subject: [PATCH 3/4] main.sh: export required env variables --- modules/soc_i3/main.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/soc_i3/main.sh b/modules/soc_i3/main.sh index 02727d47e..a4e18df5d 100755 --- a/modules/soc_i3/main.sh +++ b/modules/soc_i3/main.sh @@ -1,10 +1,10 @@ #!/bin/bash -OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd) -RAMDISKDIR="$OPENWBBASEDIR/ramdisk" -MODULEDIR=$(cd "$(dirname "$0")" && pwd) +export OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd) +export RAMDISKDIR="$OPENWBBASEDIR/ramdisk" +export MODULEDIR=$(cd "$(dirname "$0")" && pwd) LOGFILE="$RAMDISKDIR/soc.log" DMOD="EVSOC" -CHARGEPOINT=$1 +export CHARGEPOINT=$1 # check if config file is already in env if [[ -z "$debug" ]]; then From 479534620f707f090928106428a2e9f1e47bf3f6 Mon Sep 17 00:00:00 2001 From: rleidner Date: Mon, 29 Apr 2024 12:50:01 +0200 Subject: [PATCH 4/4] implement requested changes --- modules/soc_i3/i3soc.py | 55 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/modules/soc_i3/i3soc.py b/modules/soc_i3/i3soc.py index 2eae51a5c..61568ebbc 100755 --- a/modules/soc_i3/i3soc.py +++ b/modules/soc_i3/i3soc.py @@ -14,7 +14,16 @@ # ---------------Constants------------------------------------------- auth_server = 'customer.bmwgroup.com' api_server = 'cocoapi.bmwgroup.com' -REGION = '0' # rest_of_world +APIKey = b'NGYxYzg1YTMtNzU4Zi1hMzdkLWJiYjYtZjg3MDQ0OTRhY2Zh' +USER_AGENT = 'Dart/3.0 (dart:io)' +REGION = '0' # 0 = rest_of_world +BRAND = 'bmw' # for auth bmw or mini don't matter +X_USER_AGENT1 = 'android(TQ2A.230405.003.B2);' +X_USER_AGENT2 = ';3.11.1(29513);' +X_USER_AGENT = X_USER_AGENT1 + BRAND + X_USER_AGENT2 + REGION +CONTENT_TYPE = 'application/x-www-form-urlencoded' +CHARSET = 'charset=UTF-8' + storeFile = 'i3soc.json' @@ -105,9 +114,9 @@ def write_store(): json.dump(store, tf, indent=4) tf.close() try: - os.chmod(storeFile, 0o777) + os.chmod(storeFile, 0o666) except Exception as e: - os.system("sudo chmod 0777 " + storeFile) + os.system("sudo chmod 0666 " + storeFile) # ---------------HTTP Function------------------------------------------- @@ -158,12 +167,11 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' raise RuntimeError -def authStage0(region: str) -> str: +def authStage0() -> str: try: id0 = str(uuid.uuid4()) id1 = str(uuid.uuid4()) - apiKey = b'NGYxYzg1YTMtNzU4Zi1hMzdkLWJiYjYtZjg3MDQ0OTRhY2Zh' - ocp = base64.b64decode(apiKey).decode() + ocp = base64.b64decode(APIKey).decode() url = 'https://' + api_server + '/eadrax-ucs/v1/presentation/oauth/config' headers = { 'ocp-apim-subscription-key': ocp, @@ -171,8 +179,8 @@ def authStage0(region: str) -> str: 'x-identity-provider': 'gcdm', 'x-correlation-id': id1, 'bmw-correlation-Id': id1, - 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);' + region} + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} body = getHTTP(url, headers) cfg = json.loads(body) except: @@ -191,9 +199,9 @@ def authStage1(url: str, global config try: headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + 'Content-Type': CONTENT_TYPE, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} data = { 'client_id': config['clientId'], 'response_type': 'code', @@ -221,9 +229,9 @@ def authStage1(url: str, def authStage2(url: str, authcode1: str, code_challenge: str, state: str, nonce: str) -> str: try: headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + 'Content-Type': CONTENT_TYPE, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} data = { 'client_id': config['clientId'], 'response_type': 'code', @@ -253,10 +261,7 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: try: url = token_url headers = { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Authorization': (config['clientId'], config['clientSecret'])} - headers = { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} + 'Content-Type': CONTENT_TYPE + '; ' + CHARSET} data = { 'code': authcode2, 'code_verifier': code_verifier, @@ -281,7 +286,7 @@ def requestToken(username: str, password: str) -> dict: try: # new: get oauth config from server method += ' requestToken' - config = authStage0(REGION) + config = authStage0() _debug('config=\n' + json.dumps(config, indent=4)) token_url = config['tokenEndpoint'] authenticate_url = token_url.replace('/token', '/authenticate') @@ -305,12 +310,12 @@ def refreshToken(refreshToken: str) -> dict: global method try: method += ' refreshToken' - config = authStage0(REGION) + config = authStage0() url = config['tokenEndpoint'] headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);bmw;3.11.1(29513);0'} + 'Content-Type': CONTENT_TYPE, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} data = { 'scope': ' '.join(config['scopes']), 'redirect_uri': config['returnUrl'], @@ -342,8 +347,8 @@ def requestData(token: str, vin: str) -> dict: url = 'https://' + api_server + '/eadrax-vcs/v4/vehicles/state' headers = { - 'user-agent': 'Dart/3.0 (dart:io)', - 'x-user-agent': 'android(TQ2A.230405.003.B2);' + brand + ';3.11.1(29513);0', + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT1 + brand + X_USER_AGENT2 + REGION, 'bmw-vin': vin, 'Authorization': (token["token_type"] + " " + token["access_token"])} body = getHTTP(url, headers)