Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

soc_i3: implement new captcha process #2858

Merged
merged 6 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 121 additions & 41 deletions modules/soc_i3/i3soc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
import os
import time
from datetime import datetime
import urllib
import uuid
import hashlib
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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')
Expand All @@ -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())
Expand All @@ -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:
Expand All @@ -420,30 +480,51 @@ 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)
# compute expires_at and write store file
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()
_debug('main: token=\n' + json.dumps(token, indent=4))
else:
_error("requestToken failed")
store['expires_at'] = 0
store['Token'] = token
write_store()
except Exception as e:
authError("requestToken Exception: " + str(e))
raise

# get Data from Server
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"])
_info("Successful - SoC: " + str(soc) + "%" + ', method=' + method)
except:
_error("Request failed")
data = requestData(token, vin)
soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"])
_info("Successful - SoC: " + str(soc) + "%" + ', method=' + method)
except Exception as e:
_error("Request failed, exception=" + str(e))
raise

try:
Expand All @@ -453,8 +534,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
Expand Down
43 changes: 41 additions & 2 deletions modules/soc_i3/main.sh
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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"'", '
Expand Down Expand Up @@ -108,6 +146,7 @@ else
ARGS+='"socfile": "'"$socfile"'", '
ARGS+='"meterfile": "'"$meterfile"'", '
ARGS+='"statefile": "'"$statefile"'", '
ARGS+='"captcha_token": "'"$captcha_token"'", '
ARGS+='"debugLevel": "'"$DEBUGLEVEL"'"'
ARGS+='}'

Expand Down
6 changes: 6 additions & 0 deletions runs/updateConfig.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading