diff --git a/modules/soc_i3/i3soc.py b/modules/soc_i3/i3soc.py index 3eea263df..23b89e678 100755 --- a/modules/soc_i3/i3soc.py +++ b/modules/soc_i3/i3soc.py @@ -6,6 +6,7 @@ import sys import os import time +from datetime import datetime import urllib import uuid import hashlib @@ -36,20 +37,49 @@ # ---------------Helper Function------------------------------------------- +def _print(txt: str): + ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(ts + ': ' + txt) + + def _error(txt: str): - print(txt) + global CHARGEPOINT + _print("ERROR: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt) + + +def _warn(txt: str): + global CHARGEPOINT + _print("WARNING: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt) def _info(txt: str): global DEBUGLEVEL + global CHARGEPOINT if DEBUGLEVEL >= 1: - print(txt) + _print("INFO: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt) + + +def _attention(txt: str): + global CHARGEPOINT + _print("HINWEIS: " + txt) def _debug(txt: str): global DEBUGLEVEL + global CHARGEPOINT if DEBUGLEVEL > 1: - print(txt) + _print("DEBUG: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt) + + +def authError(txt: str): + global CHARGEPOINT + _attention("-------------------------------------------------------------------------------------") + _attention("Anmeldung fehlgeschlagen: " + txt) + _attention("Bitte auf folgende Seite gehen: Einstellungen - Modulkonfiguration - Ladepunkte") + _attention("In der Konfiguration des LP" + str(CHARGEPOINT) + + " im BMW & Mini SOC-Modul einen neuen Captcha Token ermitteln und eingeben.") + _attention("Weitere Hinweise zum Ermitteln des Captcha Token finden sich auf der Konfigurationsseite") + _attention("-------------------------------------------------------------------------------------") def get_random_string(length: int) -> str: @@ -78,6 +108,7 @@ def init_store(): store = {} store['Token'] = {} store['expires_at'] = int(0) + store['captcha_token'] = '' # load store from file, initialize store structure if no file exists @@ -90,8 +121,10 @@ def load_store(): if 'Token' not in store: init_store() tf.close() + if 'captcha_token' not in store: + store['captcha_token'] = '' except FileNotFoundError: - _error("load_store: store file not found, new authentication required") + _warn("load_store: store file not found, new authentication required") store = {} init_store() except Exception as e: @@ -119,6 +152,24 @@ def write_store(): os.system("sudo chmod 0666 " + storeFile) +# write state file +def write_state(): + global state + global stateFile + try: + tf = open(stateFile, 'w', encoding='utf-8') + except Exception as e: + _error("write_state_file: Exception " + str(e)) + os.system("sudo rm -f " + stateFile) + tf = open(stateFile, 'w', encoding='utf-8') + json.dump(state, tf, indent=4) + tf.close() + try: + os.chmod(stateFile, 0o666) + except Exception as e: + os.system("sudo chmod 0666 " + stateFile) + + # ---------------HTTP Function------------------------------------------- def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = 30) -> str: try: @@ -155,7 +206,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' _error("Connection Timeout") raise except: - _error("HTTP Error") + _error("HTTP Error, response=" + str(response)) raise if response.status_code == 200 or response.status_code == 204: @@ -164,6 +215,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' return response.headers["location"] else: _error('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, response.content: ' + str(response.content)) raise RuntimeError @@ -195,13 +247,12 @@ def authStage1(url: str, password: str, code_challenge: str, state: str, - nonce: str) -> str: + nonce: str, + captcha_token: str) -> str: global config try: headers = { - 'Content-Type': CONTENT_TYPE, - 'user-agent': USER_AGENT, - 'x-user-agent': X_USER_AGENT} + 'hcaptchatoken': captcha_token} data = { 'client_id': config['clientId'], 'response_type': 'code', @@ -280,7 +331,7 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: return token -def requestToken(username: str, password: str) -> dict: +def requestToken(username: str, password: str, captcha_token: str) -> dict: global config global method try: @@ -295,7 +346,7 @@ def requestToken(username: str, password: str) -> dict: state = get_random_string(22) nonce = get_random_string(22) - authcode1 = authStage1(authenticate_url, username, password, code_challenge, state, nonce) + authcode1 = authStage1(authenticate_url, username, password, code_challenge, state, nonce, captcha_token) authcode2 = authStage2(authenticate_url, authcode1, code_challenge, state, nonce) token = authStage3(token_url, authcode2, code_verifier) except: @@ -365,15 +416,19 @@ def requestData(token: str, vin: str) -> dict: # ---------------Main Function------------------------------------------- def main(): global store + global state global storeFile global DEBUGLEVEL + global CHARGEPOINT global method + global stateFile 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("DEBUGLEVEL=" + str(DEBUGLEVEL)) + OPENWBBASEDIR = os.environ.get("OPENWBBASEDIR", "undefined") + storeFile = OPENWBBASEDIR + '/data/i3/soc_i3_cp' + CHARGEPOINT + '.json' _debug('storeFile =' + storeFile) argsStr = base64.b64decode(str(sys.argv[1])).decode('utf-8') @@ -384,13 +439,14 @@ def main(): vin = str(argsDict["vin"]).upper() socfile = str(argsDict["socfile"]) meterfile = str(argsDict["meterfile"]) - statefile = str(argsDict["statefile"]) + stateFile = str(argsDict["statefile"]) + captcha_token = str(argsDict["captcha_token"]) except: _error("Parameters could not be processed") raise try: - # try to read store file from ramdisk + # try to read store file expires_in = -1 load_store() now = int(time.time()) @@ -403,9 +459,13 @@ def main(): expires_in = store['Token']['expires_in'] expires_at = store['expires_at'] token = store['Token'] + _exp_at = datetime.fromtimestamp(expires_at).strftime('%Y-%m-%d %H:%M:%S') + _exp_at2 = datetime.fromtimestamp(expires_at-120).strftime('%Y-%m-%d %H:%M:%S') + _now = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S') _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: + ', expires_at=' + str(expires_at) + ', diff=' + str(now - (expires_at - 120))) + _debug("expires_at=" + _exp_at + ", now=" + _now + ", expires_at-120=" + _exp_at2) + if now > (expires_at - 120): _debug('call refreshToken') token = refreshToken(token['refresh_token']) if 'expires_in' in token: @@ -420,30 +480,49 @@ def main(): else: expires_in = store['Token']['expires_in'] - # if refreshToken fails, call requestToken + # if refreshToken fails or token are missing, call requestToken if expires_in == -1: - _debug('call requestToken') - token = requestToken(username, password) - - # compute expires_at and store file in ramdisk + # check if there is a new captcha_token, i.e. different from the one already used + last_captcha_token = store['captcha_token'] + _debug("captcha_token = " + captcha_token) + _debug("last_captcha_token = " + last_captcha_token) + # if captcha_token is old, quit with error message + if captcha_token == last_captcha_token and captcha_token != "": + authError("Captcha Token wurde bereits verwendet.") + quit() + # if captcha_token is not defined, quit with error message + elif captcha_token == "" or captcha_token is None: + authError("Captcha Token nicht definiert.") + quit() + else: + # looks like we habe a promising captha_token + _debug('call requestToken with captcha_token: \n' + captcha_token) + try: + # store captcha_token in store to detect reuse + store['captcha_token'] = captcha_token + token = requestToken(username, password, captcha_token) + except Exception as e: + authError("requestToken Exception: " + str(e)) + raise + + # compute expires_at and write store file 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() + expires_in = int(token['expires_in']) + expires_at = now + expires_in + store['expires_at'] = expires_at + store['Token'] = token + write_store() + _debug('main: token=\n' + json.dumps(token, indent=4)) + data = requestData(token, vin) + soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"]) + _info("Successful - SoC: " + str(soc) + "%" + ', method=' + method) 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"]) - _info("Successful - SoC: " + str(soc) + "%" + ', method=' + method) - except: - _error("Request failed") + except Exception as e: + _error("Request failed, exception=" + str(e)) raise try: @@ -453,8 +532,7 @@ def main(): state["soc"] = int(soc) with open(meterfile, 'r') as f: state["meter"] = float(f.read()) - with open(statefile, 'w') as f: - f.write(json.dumps(state)) + write_state() except: _error("Saving SoC failed") raise diff --git a/modules/soc_i3/main.sh b/modules/soc_i3/main.sh index 116d3984a..661501b5d 100755 --- a/modules/soc_i3/main.sh +++ b/modules/soc_i3/main.sh @@ -1,4 +1,18 @@ #!/bin/bash + +# -- start user pi enforcement +# normally the soc module runs as user pi +# when LP Configuration is stored, it is run as user www-data +# This leads to various permission problems +# if actual user is not pi, this restarts the script as user pi +usr=`id -nu` +if [ "$usr" != "pi" ] +then + sudo -u pi -c bash "$0 $*" + exit $? +fi +# -- ending user pi enforcement + export OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd) export RAMDISKDIR="$OPENWBBASEDIR/ramdisk" export MODULEDIR=$(cd "$(dirname "$0")" && pwd) @@ -31,6 +45,7 @@ case $CHARGEPOINT in user=$i3usernames1 pass=$i3passworts1 vin=$i3vins1 + captcha_token=$i3captcha_tokens1 ;; *) # defaults to first charge point for backward compatibility @@ -47,9 +62,31 @@ case $CHARGEPOINT in user=$i3username pass=$i3passwort vin=$i3vin + captcha_token=$i3captcha_token ;; esac +# make sure folder data/i3 exists in openwb home folder +# can be executed by pi or www-data so we have to use sudo +prepare_i3DataFolder(){ + dataFolder="${OPENWBBASEDIR}/data" + i3Folder="${dataFolder}/i3" + if [ ! -d $i3Folder ] + then + sudo mkdir -p $i3Folder + f=soc_i3_cp1.json + if [ -f $RAMDISKDIR/$f && !-f $i3Folder/$f ]; then + cp $RAMDISKDIR/$f $i3Folder + fi + f=soc_i3_cp2.json + if [ -f $RAMDISKDIR/$f && !-f $i3Folder/$f ]; then + cp $RAMDISKDIR/$f $i3Folder + fi + fi + sudo chown -R pi:pi $dataFolder + sudo chmod 0777 $i3Folder +} + incrementTimer(){ case $dspeed in 1) @@ -73,12 +110,13 @@ incrementTimer(){ echo $soctimer > "$soctimerfile" } +prepare_i3DataFolder soctimer=$(<"$soctimerfile") -openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: timer = $soctimer" +openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: timer = $soctimer" cd $MODULEDIR if (( soctimer < (6 * intervall) )); then if(( soccalc < 1 )); then - openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: Nothing to do yet. Incrementing timer." + openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: Nothing to do yet. Incrementing timer." else ARGS='{' ARGS+='"socfile": "'"$socfile"'", ' @@ -108,6 +146,7 @@ else ARGS+='"socfile": "'"$socfile"'", ' ARGS+='"meterfile": "'"$meterfile"'", ' ARGS+='"statefile": "'"$statefile"'", ' + ARGS+='"captcha_token": "'"$captcha_token"'", ' ARGS+='"debugLevel": "'"$DEBUGLEVEL"'"' ARGS+='}' diff --git a/runs/updateConfig.sh b/runs/updateConfig.sh index fb53f7c5a..b2a80c702 100755 --- a/runs/updateConfig.sh +++ b/runs/updateConfig.sh @@ -329,6 +329,12 @@ updateConfig(){ if ! grep -Fq "i3vin=" $ConfigFile; then echo "i3vin=VIN" >> $ConfigFile fi + if ! grep -Fq "i3captcha_token=" $ConfigFile; then + echo "i3captcha_token=''" >> $ConfigFile + fi + if ! grep -Fq "i3captcha_tokens1=" $ConfigFile; then + echo "i3captcha_tokens1=''" >> $ConfigFile + fi if ! grep -Fq "i3_soccalclp1=" $ConfigFile; then echo "i3_soccalclp1=0" >> $ConfigFile fi diff --git a/upd.sh b/upd.sh new file mode 100755 index 000000000..52f9fd669 --- /dev/null +++ b/upd.sh @@ -0,0 +1,21 @@ +s=~/openWB +d=~/develop/github/openWB + +#if [ -f $d/modules/soc_smarteq/_secrets.py ] +#then +# rm $d/modules/soc_smarteq/_secrets.py +#fi +#if [ -f $d/modules/soc_smarteq/soc_smarteq.py ] +#then +# rm $d/modules/soc_smarteq/soc_smarteq.py +#fi +#cp $s/modules/soc_smarteq/main.sh $d/modules/soc_smarteq/main.sh +#cp $s/runs/atreboot.sh $d/runs/atreboot.sh +#cp $s/runs/updateConfig.sh $d/runs/updateConfig.sh +#cp $s/web/settings/modulconfiglp.php $d/web/settings/modulconfiglp.php +#cp $s/modules/soc_smarteq/soc_smarteq_2fa.py $d/modules/soc_smarteq/soc_smarteq_2fa.py +#cp $s/modules/soc_smarteq/soc_smarteq_pass.py $d/modules/soc_smarteq/soc_smarteq_pass.py + + +cp $s/modules/soc_vwid/soc_vwid.py $d/modules/soc_vwid/soc_vwid.py + diff --git a/web/settings/modulconfiglp.php b/web/settings/modulconfiglp.php index 6788cac96..a26f9e378 100644 --- a/web/settings/modulconfiglp.php +++ b/web/settings/modulconfiglp.php @@ -1877,6 +1877,28 @@ function visibility_socevccpinlp1() { +
+ +
+ + + Zum erstmaligen Login z.B. nach einem Neustart ist ein aktuelles Captcha-Token notwendig.
+ Dazu bitte folgende Schritte durchführen:
+ 1. in einem neuen Browser-Tab auf diese Seite gehen:
+
+ Captcha Page + + (https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html)
+ 2. Dort falls angefragt das Captcha lösen und/oder "Ich bin ein Mensch" und dann Submit anclicken.
+ Als Ergebnis wird ein sehr langer String angezeigt.
+ 3. Diesen String komplett mit Copy&Paste in das Feld Captcha-Token eingeben.
+ 4. Die Konfiguration speichern.
+ 5. Fertig. Ab jetzt wird das sog. Refresh-Token verwendet.
+ Das erneute Abrufen des Captcha-Token sollte nur notwendig werden wenn die Datei mit dem Refresh-Token verloren wird, z.B. nach SD-Kartentausch.
+ Achtung: Das Captcha-Token kann nur einmal verwendet werden und gilt nur kurze Zeit! +
+
+
@@ -3646,6 +3668,28 @@ function visibility_twcmanagerlp2_connection() {
+
+ +
+ + + Zum erstmaligen Login z.B. nach einem Neustart ist ein aktuelles Captcha-Token notwendig.
+ Dazu bitte folgende Schritte durchführen:
+ 1. in einem neuen Browser-Tab auf diese Seite gehen:
+
+ Captcha Page + + (https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html)
+ 2. Dort falls angefragt das Captcha lösen und/oder "Ich bin ein Mensch" und dann Submit anclicken.
+ Als Ergebnis wird ein sehr langer String angezeigt.
+ 3. Diesen String komplett mit Copy&Paste in das Feld Captcha-Token eingeben.
+ 4. Die Konfiguration speichern.
+ 5. Fertig. Ab jetzt wird das sog. Refresh-Token verwendet.
+ Das erneute Abrufen des Captcha-Token sollte nur notwendig werden wenn die Datei mit dem Refresh-Token verloren wird, z.B. nach SD-Kartentausch.
+ Achtung: Das Captcha-Token kann nur einmal verwendet werden und gilt nur kurze Zeit! +
+
+