From 7174b8529543a2dba72c6757ec40798826f6f595 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Thu, 22 Oct 2020 13:33:59 +0200 Subject: [PATCH] Added Python version of the script. --- .gitignore | 5 +- README.md | 23 ++- mercedes_me_api.py | 375 +++++++++++++++++++++++++++++++++++++++++++++ mercedes_me_api.sh | 4 +- 4 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 mercedes_me_api.py diff --git a/.gitignore b/.gitignore index f76a4bd..2ef5a3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -.mercedes_credentials -.mercedes_token +.mercedesme_credentials +.mercedesme_token +.mercedesme_resources diff --git a/README.md b/README.md index 69cd9de..7d3084b 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,28 @@ To use this script it's necessary to perform the following instructions: 1) clone the repository 2) create a credentials files (.mercedes_credentials) with: ```bash -CLIENT_ID="" -CLIENT_SECRET="" -VEHICLE_ID="" +CLIENT_ID=<**INSERT_YOUR_CLIENT_ID**> +CLIENT_SECRET=<**INSERT_YOUR_CLIENT_SECRET**> +VEHICLE_ID=<**INSERT_YOUR_VEHICLE_ID**> ``` where CLIENT_ID and CLIENT_SECRET referring to the application information that can be found in [Mercedes Developer Console](https://developer.mercedes-benz.com/console) and VEHICLE_ID is the VIN of your car. -## Usage +## Python Usage +To execute the script read below: +```bash +usage: mercedes_me_api.py [-h] [-t] [-r] [-s] [-R] [-v] + +optional arguments: + -h, --help show this help message and exit + -t, --token Procedure to obtatin the Access Token + -r, --refresh Procedure to refresh the Access Token + -s, --status Retrive the Status of your Vehicle + -R, --resources Retrive the list of available resources of your Vehicle + -v, --version show program's version number and exit +``` + +## Bash Usage To execute the script read below: ```bash Usage: mercedes_me_api.sh @@ -38,6 +52,7 @@ The possible arguments are: -l, --lock Retrive the Lock Status of your Vehicle -s, --status Retrive the General Status of your Vehicle ``` + ## License [MIT](http://opensource.org/licenses/MIT) © Giorgio Ravera diff --git a/mercedes_me_api.py b/mercedes_me_api.py new file mode 100644 index 0000000..5f0c220 --- /dev/null +++ b/mercedes_me_api.py @@ -0,0 +1,375 @@ +""" +Author: G. Ravera +Version 0.1 +Creation Date: 22/10/2020 + +Change log: + 22/10/2020 - 0.1 - First Issue +""" +import argparse +import base64 +from configobj import ConfigObj +import getopt +import logging +import json +import os +import requests +import sys + +# URLs +oauth_url = "https://id.mercedes-benz.com/as/token.oauth2" +res_url_prefix = "https://api.mercedes-benz.com/vehicledata/v2" +# File Parameters +CONF_CLIENT_ID = "CLIENT_ID" +CONF_CLIENT_SECRET = "CLIENT_SECRET" +CONF_VEHICLE_ID = "VEHICLE_ID" +# Config Parameters +MercedeMeConfig = { + "name": "Mercedes Me API", + "domain": "mercedesmeapi", + "version": "0.1", + "token_file": ".mercedesme_token", + "credentials_file": ".mercedesme_credentials", + "available_resources_file": ".mercedesme_resources", + "redirect_uri": "https://localhost", + "scope": "mb:vehicle:mbdata:fuelstatus%20mb:vehicle:mbdata:vehiclestatus%20mb:vehicle:mbdata:vehiclelock%20offline_access", + "client_id": "", + "client_secret": "", + "vin": "", + "base64": "", + "access_token": "", + "refresh_token": "", +} +# Available Resources (Array) +availableResources = {} +# Response Validity +valid_res = False +# Logger +logger = logging.getLogger(__name__) + +######################## +# Read Configuration +######################## +def ReadCfg(): + global availableResources + + # Read Credentials + if not os.path.isfile(MercedeMeConfig['credentials_file']): + logger.error ("Credential File " + MercedeMeConfig['credentials_file'] + " not found") + return False + f = ConfigObj(MercedeMeConfig['credentials_file']) + MercedeMeConfig['client_id'] = f.get(CONF_CLIENT_ID) + if not MercedeMeConfig['client_id']: + logger.error ("No "+ CONF_CLIENT_ID + " found in the configuration") + return False + MercedeMeConfig['client_secret'] = f.get(CONF_CLIENT_SECRET) + if not MercedeMeConfig['client_secret']: + logger.error ("No "+ CONF_CLIENT_SECRET + " found in the configuration") + return False + MercedeMeConfig['vin'] = f.get(CONF_VEHICLE_ID) + if not MercedeMeConfig['vin']: + logger.error ("No "+ CONF_VEHICLE_ID + " found in the configuration") + return False + b64_str = MercedeMeConfig['client_id'] + ":" + MercedeMeConfig['client_secret'] + b64_bytes = base64.b64encode( b64_str.encode('ascii') ) + MercedeMeConfig['base64'] = b64_bytes.decode('ascii') + + # Read Token + if not os.path.isfile(MercedeMeConfig['token_file']): + logger.error ("Token File missing - Creating a new one") + token = CreateToken() + if token == None: + logger.error ("Error creating token") + return False + else: + with open(MercedeMeConfig['token_file'], 'r') as file: + try: + token = json.load(file) + except ValueError: + token = {} + if not CheckToken(token): + logger.error ("Token File not correct - Creating a new one") + token = CreateToken() + if token == None: + return False + # Save Token + MercedeMeConfig['access_token'] = token['access_token'] + MercedeMeConfig['refresh_token'] = token['refresh_token'] + + # Get Resources List + if not os.path.isfile(MercedeMeConfig['available_resources_file']): + logger.error ("Resource File missing - Creating a new one") + return CreateResourcesFile() + else: + with open(MercedeMeConfig['available_resources_file'], 'r') as file: + try: + availableResources = json.load(file) + except ValueError: + availableResources = {} + if not CheckResources(availableResources): + logger.error ("Resource File not correct - Creating a new one") + return CreateResourcesFile() + + return True + +######################## +# Write Token +######################## +def WriteToken(token): + with open(MercedeMeConfig['token_file'], 'w') as file: + json.dump(token, file) + +######################## +# Check Token +######################## +def CheckToken(token): + if "error" in token: + if "error_description" in token: + logger.error ("Error retriving token: " + token["error_description"]) + else: + logger.error ("Error retriving token: " + token["error"]) + return False + if len(token) == 0: + logger.error ("Empty token found.") + return False + if not 'access_token' in token: + logger.error ("Access token not present.") + return False + if not 'refresh_token' in token: + logger.error ("Refresh token not present.") + return False + + return True + +######################## +# Create Token +######################## +def CreateToken(): + print( "Open the browser and insert this link:\n" ) + print( "https://id.mercedes-benz.com/as/authorization.oauth2?response_type=code&client_id=" + MercedeMeConfig["client_id"] + "&redirect_uri=" + MercedeMeConfig["redirect_uri"] + "&scope=" + MercedeMeConfig["scope"] + "\n") + print( "Copy the code in the url:") + auth_code = input() + + headers = { + "Authorization": "Basic " + MercedeMeConfig['base64'], + "content-type": "application/x-www-form-urlencoded" + } + data = "grant_type=authorization_code&code=" + auth_code + "&redirect_uri=" + MercedeMeConfig["redirect_uri"] + + res = requests.post(oauth_url, data = data, headers = headers) + try: + token = res.json() + except ValueError: + logger.error ("Error retriving token " + str(res.status_code)) + return None + + # Check Token + if not CheckToken(token): + return None + else: + WriteToken(token) + return token + +######################## +# Refresh Token +######################## +def RefreshToken(): + headers = { + "Authorization": "Basic " + MercedeMeConfig['base64'], + "content-type": "application/x-www-form-urlencoded" + } + data = "grant_type=refresh_token&refresh_token=" + MercedeMeConfig['refresh_token'] + + res = requests.post(oauth_url, data = data, headers = headers) + try: + token = res.json() + except ValueError: + logger.error ("Error refreshing token: " + str(res.status_code)) + return False + + MercedeMeConfig['access_token'] = token['access_token'] + MercedeMeConfig['refresh_token'] = token['refresh_token'] + + WriteToken(token) + + return True + +######################## +# Init Resources +######################## +def InitResources(): + # Create Empty Status + for res in availableResources: + res["status"] = "" + res["timestamp"] = 0 + res["valid"] = False + +######################## +# Write Resources File +######################## +def WriteResourcesFile(): + with open(MercedeMeConfig['available_resources_file'], 'w') as file: + json.dump(availableResources, file) + +######################## +# Check Resources +######################## +def CheckResources(resources): + if "error" in resources: + if "error_description" in resources: + logger.error ("Error retriving resources: " + resources["error_description"]) + else: + logger.error ("Error retriving resources: " + resources["error"]) + return False + if len(resources) == 0: + logger.error ("Empty resources found.") + return False + + return True + +######################## +# Create Resources File +######################## +def CreateResourcesFile(): + global availableResources + resName = "resources" + resURL = res_url_prefix + "/vehicles/" + MercedeMeConfig['vin'] + "/" + resName + availableResources = GetResource(resName, resURL) + if valid_res: + InitResources() + WriteResourcesFile() + return True + else: + logger.error ("Error retriving available resources") + logger.error ("-> " + availableResources["reason"] + " (" + str(availableResources["code"]) + ")") + return False + +######################## +# GetResource +######################## +def GetResource(resourceName, resourceURL): + global valid_res + + # Set Header + headers = { + "accept": "application/json;charset=utf-8", + "authorization": "Bearer "+MercedeMeConfig['access_token'] + } + + # Send Request + res = requests.get(resourceURL, headers=headers) + try: + data = res.json() + valid_res = True + except ValueError: + data = { "reason": "No Data", + "code" : res.status_code + } + valid_res = False + + # Check Error + if not res.ok: + if ("reason" in data): + reason = data["reason"] + else: + if res.status_code == 400: + reason = "Bad Request" + elif res.status_code == 401: + reason = "Invalid or missing authorization in header" + elif res.status_code == 402: + reason = "Payment required" + elif res.status_code == 403: + reason = "Forbidden" + elif res.status_code == 404: + reason = "Page not found" + elif res.status_code == 429: + reason = "The service received too many requests in a given amount of time" + elif res.status_code == 500: + reason = "An error occurred on the server side" + elif res.status_code == 503: + reason = "The server is unable to service the request due to a temporary unavailability condition" + else: + reason = "Generic Error" + data["reason"] = reason + data["code"] = res.status_code + valid_res = False + return data + +######################## +# Print Available Resources +######################## +def PrintAvailableResources(): + print ("Found %d resources" % len(availableResources) + ":") + for res in availableResources: + print (res["name"] + ": " + res_url_prefix + res["href"]) + +######################## +# Print Resources Status +######################## +def PrintResourcesStatus(valid = True): + for res in availableResources: + if((not valid) | res["valid"]): + print (res["name"] + ":") + print ("\tvalid: " + str(res["valid"])) + print ("\tstatus: " + res["status"]) + print ("\ttimestamp: " + str(res["timestamp"])) + +######################## +# Update Resources Status +######################## +def UpdateResourcesStatus(): + for res in availableResources: + resName = res["name"] + resURL = res_url_prefix + res["href"] + result = GetResource(resName, resURL) + if valid_res: + res["valid"] = True + res["timestamp"] = result[resName]["timestamp"] + res["status"] = result[resName]["value"] + # Write Resource File + WriteResourcesFile() + +######################## +# Parse Input +######################## +def ParseInput(): + parser = argparse.ArgumentParser() + parser.add_argument('-t', '--token', action='store_true', help="Procedure to obtatin the Access Token") + parser.add_argument('-r', '--refresh', action='store_true', help="Procedure to refresh the Access Token") + parser.add_argument('-s', '--status', action='store_true', help="Retrive the Status of your Vehicle") + parser.add_argument('-R', '--resources', action='store_true', help="Retrive the list of available resources of your Vehicle") + parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + MercedeMeConfig['version']) + + if len(sys.argv)==1: + parser.print_help(sys.stderr) + exit(1) + + return parser.parse_args() + +######################## +# Main +######################## +if __name__ == "__main__": + args = ParseInput() + + # Reading Configuration + if not ReadCfg(): + logger.error ("Error reading configuration") + exit (1) + + if ( args.token == True): + if not CreateToken(): + logger.error ("Error creating token") + exit (1) + + if (args.refresh == True): + if not RefreshToken(): + logger.error ("Error refreshing token") + exit (1) + + if (args.resources): + PrintAvailableResources() + + if (args.status == True): + UpdateResourcesStatus() + PrintResourcesStatus() diff --git a/mercedes_me_api.sh b/mercedes_me_api.sh index 5941877..01b4dcb 100755 --- a/mercedes_me_api.sh +++ b/mercedes_me_api.sh @@ -16,8 +16,8 @@ VERSION="0.1" REDIRECT_URL="https://localhost" SCOPE="mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock offline_access" STATE="12345678" -TOKEN_FILE=".mercedes_token" -CREDENTIALS_FILE=".mercedes_credentials" +TOKEN_FILE=".mercedesme_token" +CREDENTIALS_FILE=".mercedesmw_credentials" # Credentials CLIENT_ID="" -- 2.47.3