From: Giorgio Ravera Date: Thu, 5 Nov 2020 19:04:30 +0000 (+0100) Subject: Aligned home assistant component with standalone python version. X-Git-Tag: v0.3~12 X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=f707fd8cd8bea96264d4310cc6aa4c6b3c132dae;p=mercedes_me_api.git Aligned home assistant component with standalone python version. --- diff --git a/README.md b/README.md index 3decba9..05c34ca 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Note2: only one car is supported for the moment. The Home Assistant Custom Component is a component to be added in Home Assistant in order to integrate sensors of a Mercedes Benz car. This component is still in development. ### Open Points -- Fix OAUTH2 Authentication & Get the First Token +- Complete OAUTH2 Authentication & Get the First Token +- Get state after starts -> now it waits seconds. +- Config Flow for automatic configuration - Log Management - Bugfix & Software optimizations diff --git a/custom_components/mercedesmeapi/__init__.py b/custom_components/mercedesmeapi/__init__.py index 360c117..19d4e0e 100644 --- a/custom_components/mercedesmeapi/__init__.py +++ b/custom_components/mercedesmeapi/__init__.py @@ -10,6 +10,7 @@ from datetime import timedelta import logging from .config import MercedesMeConfig +from .oauth import MercedesMeOauth from .resources import MercedesMeResources from .const import * @@ -37,11 +38,11 @@ class MercedesMeData: _LOGGER.debug ("End Update") ######################## - # Update State + # Update Token ######################## def UpdateToken(self, event_time): _LOGGER.debug ("Start Refresh Token") - self.mercedesConfig.RefreshToken() + self.mercedesConfig.token.RefreshToken() #hass.helpers.dispatcher.dispatcher_send(UPDATE_SIGNAL) _LOGGER.debug ("End Refresh Token") @@ -73,6 +74,6 @@ def setup(hass, config): hass.helpers.event.track_time_interval(data.UpdateState, timedelta(seconds=data.mercedesConfig.scan_interval)) # Create Task to Update Token - hass.helpers.event.track_time_interval(data.UpdateToken, timedelta(seconds=data.mercedesConfig.token_expires_in)) + hass.helpers.event.track_time_interval(data.UpdateToken, timedelta(seconds=data.mercedesConfig.token.token_expires_in)) return True \ No newline at end of file diff --git a/custom_components/mercedesmeapi/config.py b/custom_components/mercedesmeapi/config.py index 86368fe..afd07bd 100644 --- a/custom_components/mercedesmeapi/config.py +++ b/custom_components/mercedesmeapi/config.py @@ -6,11 +6,7 @@ Author: G. Ravera For more details about this component, please refer to the documentation at https://github.com/xraver/mercedes_me_api/ """ -import base64 -import json import logging -import os -import requests import voluptuous as vol from homeassistant.const import ( @@ -21,10 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery, config_validation as cv -from .query import ( - GetResource, - GetToken -) +from .oauth import MercedesMeOauth from .const import * # Logger @@ -48,18 +41,6 @@ CONFIG_SCHEMA = vol.Schema ( class MercedesMeConfig: - token_file = "" - credentials_file = "" - resources_file = "" - client_id = "" - client_secret = "" - vin = "" - scan_interval = "" - base64 = "" - access_token = "" - refresh_token = "" - token_expires_in = "" - ######################## # Init ######################## @@ -67,9 +48,10 @@ class MercedesMeConfig: # Home Assistant structures self.hass = hass self.config = config - # Files - self.token_file = self.hass.config.path(TOKEN_FILE) - self.resources_file = self.hass.config.path(RESOURCES_FILE) + self.client_id = "" + self.client_secret = "" + self.vin = "" + self.token = "" ######################## # Read Configuration @@ -86,119 +68,9 @@ class MercedesMeConfig: self.vin = config[CONF_VEHICLE_ID] # Scan Interval self.scan_interval = config[CONF_SCAN_INTERVAL] - # Base64 - b64_str = self.client_id + ":" + self.client_secret - b64_bytes = base64.b64encode( b64_str.encode('ascii') ) - self.base64 = b64_bytes.decode('ascii') - # Read Token - if not os.path.isfile(self.token_file): - _LOGGER.error ("Token File missing - Creating a new one") - #if (not self.CreateToken()): GRGR -> to be fixed - if (True): - _LOGGER.error ("Error creating token") - return False - else: - with open(self.token_file, 'r') as file: - try: - token = json.load(file) - except ValueError: - token = None - if self.CheckToken(token): - # Save Token - self.access_token = token['access_token'] - self.refresh_token = token['refresh_token'] - self.token_expires_in = token['expires_in'] - needToRefresh = True - else: - _LOGGER.error ("Token File not correct - Creating a new one") - #if (not self.CreateToken()): - if (True): # GRGR -> to be fixed - _LOGGER.error ("Error creating token") - return False - - if (needToRefresh): - if (not self.RefreshToken()): - _LOGGER.error ("Error refreshing token") - return False - - return True - - ######################## - # Write Token - ######################## - def WriteToken(self, token): - with open(self.token_file, 'w') as file: - json.dump(token, file) - - ######################## - # Check Token - ######################## - def CheckToken(self, token): - if token == None: - _LOGGER.error ("Error reading token.") - return False - 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(self): - - auth_url = ( - f"{URL_OAUTH_AUTH}&" + - f"client_id={self.client_id}&" + - f"redirect_uri={REDIRECT_URL}&" + - f"scope={SCOPE}" - ) - - print( "Open the browser and insert this link:\n" ) - print(auth_url + "\n") - print( "Copy the code in the url:") - auth_code = input() - - token = GetToken(self, refresh=False, auth_code=auth_code) - - # Check Token - if not self.CheckToken(token): - return False - else: - # Save Token - self.WriteToken(token) - self.access_token = token['access_token'] - self.refresh_token = token['refresh_token'] - self.token_expires_in = token['expires_in'] - return True - - ######################## - # Refresh Token - ######################## - def RefreshToken(self): - - token = GetToken(self, refresh=True) - - # Check Token - if not self.CheckToken(token): + self.token = MercedesMeOauth(self.hass, self.client_id, self.client_secret) + if not self.token.ReadToken(): return False - else: - # Save Token - self.WriteToken(token) - self.access_token = token['access_token'] - self.refresh_token = token['refresh_token'] - self.token_expires_in = token['expires_in'] + return True diff --git a/custom_components/mercedesmeapi/const.py b/custom_components/mercedesmeapi/const.py index 6d1a20c..ee347ea 100644 --- a/custom_components/mercedesmeapi/const.py +++ b/custom_components/mercedesmeapi/const.py @@ -15,9 +15,6 @@ CREDENTIAL_FILE = ".mercedesme_credentials" RESOURCES_FILE = ".mercedesme_resources" REDIRECT_URL = "https://localhost" SCOPE = "mb:vehicle:mbdata:fuelstatus%20mb:vehicle:mbdata:vehiclestatus%20mb:vehicle:mbdata:vehiclelock%20offline_access" -URL_OAUTH_BASE = "https://id.mercedes-benz.com/as" -URL_OAUTH_AUTH = URL_OAUTH_BASE + "/authorization.oauth2?response_type=code" -URL_OAUTH_TOKEN = URL_OAUTH_BASE + "/token.oauth2" URL_RES_PREFIX = "https://api.mercedes-benz.com/vehicledata/v2" #UPDATE_SIGNAL = "mercedesmeapi_update" diff --git a/custom_components/mercedesmeapi/oauth.py b/custom_components/mercedesmeapi/oauth.py new file mode 100644 index 0000000..ac50638 --- /dev/null +++ b/custom_components/mercedesmeapi/oauth.py @@ -0,0 +1,175 @@ +""" +Mercedes Me APIs + +Author: G. Ravera + +For more details about this component, please refer to the documentation at +https://github.com/xraver/mercedes_me_api/ +""" +import base64 +import json +import logging +import os +from .const import * +from .query import * + +URL_OAUTH_BASE = "https://id.mercedes-benz.com/as" +URL_OAUTH_AUTH = URL_OAUTH_BASE + "/authorization.oauth2?response_type=code" +URL_OAUTH_TOKEN = URL_OAUTH_BASE + "/token.oauth2" + +# Logger +_LOGGER = logging.getLogger(__name__) + +class MercedesMeOauth: + + ######################## + # Init + ######################## + def __init__(self, hass, client_id, client_secret): + # Access Token + self.access_token = "" + # Refresh Token + self.refresh_token = "" + # Expiration Time + self.token_expires_in = "" + # Client ID + self.client_id = client_id + # Client Secret + self.client_secret = client_secret + # Token File + self.token_file = hass.config.path(TOKEN_FILE) + # Base64 + b64_str = client_id + ":" + client_secret + b64_bytes = base64.b64encode( b64_str.encode('ascii') ) + self.base64 = b64_bytes.decode('ascii') + # Headers + self.headers = { + "Authorization": "Basic " + self.base64, + "content-type": "application/x-www-form-urlencoded" + } + + ######################## + # Read Token + ######################## + def ReadToken(self): + + found = False + needToRefresh = False + + # Read Token + if not os.path.isfile(self.token_file): + # Token File not present - Creating new one + _LOGGER.error ("Token File missing - Creating a new one") + found = False + else: + with open(self.token_file, 'r') as file: + try: + token = json.load(file) + if not self.CheckToken(token): + raise ValueError + else: + found = True + except ValueError: + _LOGGER.error ("Error reading token file - Creating a new one") + found = False + + if ( not found ): + # Not valid or file missing + #if (not self.CreateToken()): GRGR -> to be fixed + if (True): + _LOGGER.error ("Error creating token") + return False + else: + # Valid: just import + self.access_token = token['access_token'] + self.refresh_token = token['refresh_token'] + self.token_expires_in = token['expires_in'] + needToRefresh = True + + if (needToRefresh): + if (not self.RefreshToken()): + _LOGGER.error ("Error refreshing token") + return False + + return True + + ######################## + # Write Token + ######################## + def WriteToken(self, token): + with open(self.token_file, 'w') as file: + json.dump(token, file) + + ######################## + # Check Token + ######################## + def CheckToken(self, token): + if "reason" in token: + _LOGGER.error ("Error retriving token - " + token["reason"] + " (" + str(token["code"]) + ")") + return False + 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(self): + + auth_url = ( + f"{URL_OAUTH_AUTH}&" + + f"client_id={self.client_id}&" + + f"redirect_uri={REDIRECT_URL}&" + + f"scope={SCOPE}" + ) + + print( "Open the browser and insert this link:\n" ) + print(auth_url + "\n") + print( "Copy the code in the url:") + auth_code = input() + + data = "grant_type=authorization_code&code=" + auth_code + "&redirect_uri=" + REDIRECT_URL + token = GetToken(URL_OAUTH_TOKEN, self.headers, data) + + # Check Token + if not self.CheckToken(token): + return False + else: + # Save Token + self.WriteToken(token) + self.access_token = token['access_token'] + self.refresh_token = token['refresh_token'] + self.token_expires_in = token['expires_in'] + return True + + ######################## + # Refresh Token + ######################## + def RefreshToken(self): + + data = "grant_type=refresh_token&refresh_token=" + self.refresh_token + token = GetToken(URL_OAUTH_TOKEN, self.headers, data) + + # Check Token + if not self.CheckToken(token): + return False + else: + # Save Token + self.WriteToken(token) + self.access_token = token['access_token'] + self.refresh_token = token['refresh_token'] + self.token_expires_in = token['expires_in'] + return True diff --git a/custom_components/mercedesmeapi/query.py b/custom_components/mercedesmeapi/query.py index 2fde861..c945be3 100644 --- a/custom_components/mercedesmeapi/query.py +++ b/custom_components/mercedesmeapi/query.py @@ -17,12 +17,12 @@ _LOGGER = logging.getLogger(__name__) ######################## # GetResource ######################## -def GetResource(resourceName, resourceURL, config): +def GetResource(resourceURL, config): # Set Header headers = { "accept": "application/json;charset=utf-8", - "authorization": "Bearer "+ config.access_token + "authorization": "Bearer "+ config.token.access_token } # Send Request @@ -64,24 +64,30 @@ def GetResource(resourceName, resourceURL, config): ######################## # GetToken ######################## -def GetToken(config, refresh=True, auth_code=""): - headers = { - "Authorization": "Basic " + config.base64, - "content-type": "application/x-www-form-urlencoded" - } - - if (not refresh): - # New Token - data = "grant_type=authorization_code&code=" + auth_code + "&redirect_uri=" + REDIRECT_URL - else: - # Refresh - data = "grant_type=refresh_token&refresh_token=" + config.refresh_token - - res = requests.post(URL_OAUTH_TOKEN, data = data, headers = headers) +def GetToken(tokenURL, headers, data): + res = requests.post(tokenURL, data = data, headers = headers) try: - token = res.json() + data = res.json() except ValueError: _LOGGER.error ("Error retriving token " + str(res.status_code)) - return None + data = { "reason": "No Data", + "code" : res.status_code + } + + # Check Error + if not res.ok: + if ("reason" in data): + reason = data["reason"] + else: + if res.status_code == 302: + reason = "The request scope is invalid" + elif res.status_code == 400: + reason = "The redirect_uri differs from the registered one" + elif res.status_code == 401: + reason = "The specified client ID is invalid" + else: + reason = "Generic Error" + data["reason"] = reason + data["code"] = res.status_code - return token + return data diff --git a/custom_components/mercedesmeapi/resources.py b/custom_components/mercedesmeapi/resources.py index 90dcd65..6c3754b 100644 --- a/custom_components/mercedesmeapi/resources.py +++ b/custom_components/mercedesmeapi/resources.py @@ -14,8 +14,8 @@ import os from homeassistant.helpers.entity import Entity from .config import MercedesMeConfig -from .query import GetResource from .const import * +from .query import * # Logger _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,6 @@ class MercedesMeResource (Entity): self._version = version self._href = href self._vin = vin - self._vin = "mercedes" # GRGR -> test self._state = state self._timestamp = timestamp self._valid = valid @@ -85,6 +84,7 @@ class MercedesMeResources: self.database = [] self.mercedesConfig = mercedesConfig + self.resources_file = mercedesConfig.hass.config.path(RESOURCES_FILE) ######################## # Read Resources @@ -94,12 +94,12 @@ class MercedesMeResources: found = False resources = None - if not os.path.isfile(self.mercedesConfig.resources_file): + if not os.path.isfile(self.resources_file): # Resources File not present - Retriving new one from server _LOGGER.error ("Resource File missing - Creating a new one.") found = False else: - with open(self.mercedesConfig.resources_file, 'r') as file: + with open(self.resources_file, 'r') as file: try: resources = json.load(file) if (not self.CheckResources(resources)): @@ -149,9 +149,8 @@ class MercedesMeResources: # Retrive Resources List ######################## def RetriveResourcesList(self): - resName = "resources" - resURL = URL_RES_PREFIX + "/vehicles/" + self.mercedesConfig.vin + "/" + resName - resources = GetResource(resName, resURL, self.mercedesConfig) + resURL = URL_RES_PREFIX + "/vehicles/" + self.mercedesConfig.vin + "/resources" + resources = GetResource(resURL, self.mercedesConfig) if not self.CheckResources(resources): _LOGGER.error ("Error retriving available resources") return None @@ -177,7 +176,7 @@ class MercedesMeResources: for res in self.database: output.append( res.getJson() ) # Write File - with open(self.mercedesConfig.resources_file, 'w') as file: + with open(self.resources_file, 'w') as file: json.dump(output, file) ######################## @@ -204,12 +203,10 @@ class MercedesMeResources: ######################## def UpdateResourcesState(self): for res in self.database: - resName = res._name - resURL = URL_RES_PREFIX + res._href - result = GetResource(resName, resURL, self.mercedesConfig) + result = GetResource(URL_RES_PREFIX + res._href, self.mercedesConfig) if not "reason" in result: res._valid = True - res._timestamp = result[resName]["timestamp"] - res._state = result[resName]["value"] + res._timestamp = result[res._name]["timestamp"] + res._state = result[res._name]["value"] # Write Resource File self.WriteResourcesFile() diff --git a/resources.py b/resources.py index 5691f0e..09528d5 100644 --- a/resources.py +++ b/resources.py @@ -80,8 +80,8 @@ class MercedesMeResources: def __init__(self, mercedesConfig): self.database = [] - self.resources_file = RESOURCES_FILE self.mercedesConfig = mercedesConfig + self.resources_file = RESOURCES_FILE ######################## # Read Resources