From 92397bec6778029ddf41a42e54ca0cbf409e464d Mon Sep 17 00:00:00 2001 From: Simon Gillet Date: Tue, 15 Jan 2019 09:48:38 +0100 Subject: [PATCH] Better login, handle wrong token --- PyViCare/PyViCare.py | 144 +++++++++++++++++++++++-------------------- PyViCare/__init__.py | 2 + setup.py | 2 +- 3 files changed, 79 insertions(+), 69 deletions(-) create mode 100644 PyViCare/__init__.py diff --git a/PyViCare/PyViCare.py b/PyViCare/PyViCare.py index a5f85beb..1008a20d 100644 --- a/PyViCare/PyViCare.py +++ b/PyViCare/PyViCare.py @@ -7,6 +7,7 @@ import os import logging import urllib +from pickle import UnpicklingError client_id = '79742319e39245de5f91d15ff4cac2a8'; client_secret = '8ad97aceb92c5892e102b093c7c083fa'; @@ -19,18 +20,18 @@ logger.addHandler(logging.NullHandler()) # TODO Holiday program can still be used (parameters are there) heating.circuits.0.operating.programs.holiday -# TODO heating.dhw.schedule/setSchedule +# TODO heating.dhw.schedule/setSchedule # TODO handle multi install / multi devices """"Viessmann ViCare API Python tools""" class ViCareSession: """This class connects to the Viesmann ViCare API. - The authentication is done through OAuth2. + The authentication is done through OAuth2. Note that currently, a new token is generate for each run. """ - - + + def __init__(self, username, password,token_file=None): """Init function. Create the necessary oAuth2 sessions Parameters @@ -43,14 +44,14 @@ def __init__(self, username, password,token_file=None): Returns ------- """ - + self.username= username - self.password= urllib.parse.quote_plus(password) + self.password= password self.token_file=token_file self.oauth=self.__restoreToken(token_file) self._getInstallations() - logger.info("init done") - + logger.info("Initialisation successful !") + def __restoreToken(self,token_file=None): """Create the necessary oAuth2 sessions Restore it from token_file if existing (token dict) @@ -66,18 +67,21 @@ def __restoreToken(self,token_file=None): Returns ------- - oauth: + oauth: oauth sessions object """ - if os.path.isfile(token_file): - # TODO better handle incorrect token file - logger.warning("Token file argument present ") - oauth = OAuth2Session(client_id,token=self._deserializeToken(token_file)) + if (token_file!=None) and os.path.isfile(token_file): + try: + logger.debug("Token file exists") + oauth = OAuth2Session(client_id,token=self._deserializeToken(token_file)) + except UnpicklingError: + logger.warning("Could not restore token") + oauth = self.getNewToken(self.username, self.password,token_file) else: - logger.warning("Requesting new token") + logger.debug("Token file argument not provided or file does not exist") oauth = self.getNewToken(self.username, self.password,token_file) return oauth - + def getNewToken(self, username, password,token_file=None): """Create a new oAuth2 sessions Viessmann tokens expire after 3600s (60min) @@ -92,16 +96,18 @@ def getNewToken(self, username, password,token_file=None): Returns ------- - oauth: + oauth: oauth sessions object """ oauth = OAuth2Session(client_id, redirect_uri=redirect_uri,scope=viessmann_scope) authorization_url, state = oauth.authorization_url(authorizeURL) codestring="" - logger.debug(authorization_url) + logger.debug("Auth URL is: "+authorization_url) + try: header = {'Content-Type': 'application/x-www-form-urlencoded'} - response = requests.post(authorization_url, headers=header,auth=(username, password)) + response = requests.post(authorization_url, headers=header,auth=(username,password)) + logger.warning("Reeived an HTML answer from the server during auth, this is not normal:") logger.debug(response.content) except requests.exceptions.InvalidSchema as e: #capture the error, which contains the code the authorization code and put this in to codestring @@ -111,20 +117,22 @@ def getNewToken(self, username, password,token_file=None): codestring=match.group(1) logger.debug("Codestring : "+codestring) oauth.fetch_token(token_url, client_secret=client_secret,authorization_response=authorization_url,code=codestring) + logger.debug("Token received: ") logger.debug(oauth) - logger.debug("Start serial") - if token_file != None: - print() - self._serializeToken(oauth.token,token_file) - logger.info("Token serialized to "+token_file) - logger.info("New token created") - return oauth + logger.debug("Start serial") + if token_file != None: + self._serializeToken(oauth.token,token_file) + logger.info("Token serialized to "+token_file) + logger.info("New token created") + return oauth + + # TODO tranform to exception """Get URL using OAuth session. Automatically renew the token if needed Parameters ---------- url : str URL to get - + Returns ------- result: json @@ -139,7 +147,7 @@ def __get(self,url): self.oauth=self.getNewToken(self.username,self.password,self.token_file) print("token renewed") return self.__get(url) - + """POST URL using OAuth session. Automatically renew the token if needed Parameters ---------- @@ -147,12 +155,12 @@ def __get(self,url): URL to get data : str Data to post - + Returns ------- result: json json representation of the answer - """ + """ def __post(self,url,data): h = {"Content-Type":"application/json","Accept":"application/vnd.siren+json"} try: @@ -169,19 +177,19 @@ def __post(self,url,data): logger.warning("Token expired, renewing") self.oauth=self.getNewToken(self.username,self.password,self.token_file) print("token renewed") - return self._post(url,data) - + return self._post(url,data) + def _serializeToken(self,oauth,token_file): binary_file = open(token_file,mode='wb') s_token = pickle.dump(oauth,binary_file) binary_file.close() print(s_token) - + def _deserializeToken(self,token_file): binary_file = open(token_file,mode='rb') s_token = pickle.load(binary_file) return s_token - + def _getInstallations(self): self.installations = self.__get(apiURLBase+"/general-management/installations?expanded=true&") logger.debug(self.installations) @@ -189,15 +197,15 @@ def _getInstallations(self): self.id=self.installations["entities"][0]["properties"]["id"] self.serial=self.installations["entities"][0]["entities"][0]["properties"]["serial"] return self.installations - + def getInstallations(self): return self.installations - + def getProperty(self,property_name): url = apiURLBase + '/operational-data/installations/' + str(self.id) + '/gateways/' + str(self.serial) + '/devices/0/features/' + property_name j=self.__get(url) return j - + def setProperty(self,property_name,action,data): url = apiURLBase + '/operational-data/v1/installations/' + str(self.id) + '/gateways/' + str(self.serial) + '/devices/0/features/' + property_name +"/"+action return self.__post(url,data) @@ -206,7 +214,7 @@ def setProperty(self,property_name,action,data): ---------- mode : str Valid mode can be obtained using getModes() - + Returns ------- result: json @@ -215,11 +223,11 @@ def setProperty(self,property_name,action,data): def setMode(self,mode): r=self.setProperty("heating.circuits.0.operating.modes.active","setMode","{\"mode\":\""+mode+"\"}") return r - + # Works for normal, reduced, comfort # active has no action # exetenral , standby no action - # holiday, sheculde and unscheduled + # holiday, sheculde and unscheduled # activate, decativate comfort,eco """ Set the target temperature for the target program Parameters @@ -228,7 +236,7 @@ def setMode(self,mode): Can be normal, reduced or comfort temperature: int target temperature - + Returns ------- result: json @@ -236,16 +244,16 @@ def setMode(self,mode): """ def setProgramTemperature(self,program: str,temperature :int): return self.setProperty("heating.circuits.0.operating.programs."+program,"setTemperature","{\"targetTemperature\":"+str(temperature)+"}") - + def setReducedTemperature(self,temperature): return self.setProgramTemperature("reduced",temperature) - + def setComfortTemperature(self,temperature): return self.setProgramTemperature("comfort",temperature) - + def setNormalTemperature(self,temperature): return self.setProgramTemperature("normal",temperature) - + """ Activate a program NOTE DEVICE_COMMUNICATION_ERROR can just mean that the program is already on @@ -253,7 +261,7 @@ def setNormalTemperature(self,temperature): ---------- program : str Appears to work only for comfort - + Returns ------- result: json @@ -262,7 +270,7 @@ def setNormalTemperature(self,temperature): # optional temperature parameter could be passed (but not done) def activateProgram(self,program): return self.setProperty("heating.circuits.0.operating.programs."+program,"activate","{}") - + def activateComfort(self): return self.activateProgram("comfort") """ Deactivate a program @@ -270,7 +278,7 @@ def activateComfort(self): ---------- program : str Appears to work only for comfort and eco (coming from normal, can be reached only by deactivating another state) - + Returns ------- result: json @@ -280,13 +288,13 @@ def deactivateProgram(self,program): return self.setProperty("heating.circuits.0.operating.programs."+program,"deactivate","{}") def deactivateComfort(self): return self.deactivateProgram("comfort") - + """ Set the target temperature for domestic host water Parameters ---------- temperature : int Target temperature - + Returns ------- result: json @@ -294,109 +302,109 @@ def deactivateComfort(self): """ def setDomesticHotWaterTemperature(self,temperature): return self.setProperty("heating.dhw.temperature","setTargetTemperature","{\"temperature\":"+str(temperature)+"}") - + def getMonthSinceLastService(self): try: return self.getProperty("heating.service.timeBased")["properties"]["activeMonthSinceLastService"]["value"] except KeyError: return "error" - + def getLastServiceDate(self): try: return self.getProperty("heating.service.timeBased")["properties"]["lastService"]["value"] except KeyError: return "error" - + def getOutsideTemperature(self): try: return self.getProperty("heating.sensors.temperature.outside")["properties"]["value"]["value"] except KeyError: return "error" - + def getSupplyTemperature(self): try: return self.getProperty("heating.circuits.0.sensors.temperature.supply")["properties"]["value"]["value"] except KeyError: return "error" - + def getRoomTemperature(self): try: return self.getProperty("heating.circuits.0.sensors.temperature.room")["properties"]["value"]["value"] except KeyError: return "error" - + def getModes(self): try: return self.getProperty("heating.circuits.0.operating.modes.active")["actions"][0]["fields"][0]["enum"] except KeyError: return "error" - + def getActiveMode(self): try: return self.getProperty("heating.circuits.0.operating.modes.active")["properties"]["value"]["value"] except KeyError: return "error" - + def getHeatingCurveShift(self): try: return self.getProperty("heating.circuits.0.heating.curve")["properties"]["shift"]["value"] except KeyError: return "error" - + def getHeatingCurveSlope(self): try: return self.getProperty("heating.circuits.0.heating.curve")["properties"]["slope"]["value"] except KeyError: return "error" - + def getBoilerTemperature(self): try: return self.getProperty("heating.boiler.sensors.temperature.main")["properties"]["value"]["value"] except KeyError: return "error" - + def getActiveProgram(self): try: return self.getProperty("heating.circuits.0.operating.programs.active")["properties"]["value"]["value"] except KeyError: return "error" - + def getPrograms(self): try: return self.getProperty("heating.circuits.0.operating.programs")["entities"][9]["properties"]["components"] except KeyError: return "error" - + def getDesiredTemperatureForProgram(self , program): try: return self.getProperty("heating.circuits.0.operating.programs."+program)["properties"]["temperature"]["value"] except KeyError: return "error" - + def getCurrentDesiredTemperature(self): try: return self.getProperty("heating.circuits.0.operating.programs."+self.getActiveProgram())["properties"]["temperature"]["value"] except KeyError: return "error" - + def getDomesticHotWaterConfiguredTemperature(self): try: return self.getProperty("heating.dhw.temperature")["properties"]["value"]["value"] except KeyError: return "error" - + def getDomesticHotWaterStorageTemperature(self): try: return self.getProperty("heating.dhw.sensors.temperature.hotWaterStorage")["properties"]["value"]["value"] except KeyError: - return "error" - + return "error" + def getDomesticHotWaterMaxTemperature(self): try: return self.getProperty("heating.dhw.temperature")["actions"][0]["fields"][0]["max"] except KeyError: return "error" - + def getDomesticHotWaterMinTemperature(self): try: return self.getProperty("heating.dhw.temperature")["actions"][0]["fields"][0]["min"] diff --git a/PyViCare/__init__.py b/PyViCare/__init__.py new file mode 100644 index 00000000..cfd0e1e6 --- /dev/null +++ b/PyViCare/__init__.py @@ -0,0 +1,2 @@ +import logging +from .PyViCare import ViCareSession diff --git a/setup.py b/setup.py index f7e30603..131dc98e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="PyViCare", - version="0.0.13", + version="0.0.20", author="Simon Gillet", author_email="mail+pyvicare@gillet.ninja", description="Library to communicate with the Viessmann ViCare API",