Skip to content

Commit 11ecd20

Browse files
authored
Merge pull request #2858 from rleidner/soc_bmw_p4
soc_i3: implement new captcha process
2 parents 49dabaf + a71dcc4 commit 11ecd20

File tree

4 files changed

+212
-43
lines changed

4 files changed

+212
-43
lines changed

modules/soc_i3/i3soc.py

Lines changed: 121 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import os
88
import time
9+
from datetime import datetime
910
import urllib
1011
import uuid
1112
import hashlib
@@ -36,20 +37,49 @@
3637

3738
# ---------------Helper Function-------------------------------------------
3839

40+
def _print(txt: str):
41+
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
42+
print(ts + ': ' + txt)
43+
44+
3945
def _error(txt: str):
40-
print(txt)
46+
global CHARGEPOINT
47+
_print("ERROR: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt)
48+
49+
50+
def _warn(txt: str):
51+
global CHARGEPOINT
52+
_print("WARNING: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt)
4153

4254

4355
def _info(txt: str):
4456
global DEBUGLEVEL
57+
global CHARGEPOINT
4558
if DEBUGLEVEL >= 1:
46-
print(txt)
59+
_print("INFO: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt)
60+
61+
62+
def _attention(txt: str):
63+
global CHARGEPOINT
64+
_print("HINWEIS: " + txt)
4765

4866

4967
def _debug(txt: str):
5068
global DEBUGLEVEL
69+
global CHARGEPOINT
5170
if DEBUGLEVEL > 1:
52-
print(txt)
71+
_print("DEBUG: soc_i3:LP" + str(CHARGEPOINT) + ": " + txt)
72+
73+
74+
def authError(txt: str):
75+
global CHARGEPOINT
76+
_attention("-------------------------------------------------------------------------------------")
77+
_attention("Anmeldung fehlgeschlagen: " + txt)
78+
_attention("Bitte auf folgende Seite gehen: Einstellungen - Modulkonfiguration - Ladepunkte")
79+
_attention("In der Konfiguration des LP" + str(CHARGEPOINT) +
80+
" im BMW & Mini SOC-Modul einen neuen Captcha Token ermitteln und eingeben.")
81+
_attention("Weitere Hinweise zum Ermitteln des Captcha Token finden sich auf der Konfigurationsseite")
82+
_attention("-------------------------------------------------------------------------------------")
5383

5484

5585
def get_random_string(length: int) -> str:
@@ -78,6 +108,7 @@ def init_store():
78108
store = {}
79109
store['Token'] = {}
80110
store['expires_at'] = int(0)
111+
store['captcha_token'] = ''
81112

82113

83114
# load store from file, initialize store structure if no file exists
@@ -90,8 +121,10 @@ def load_store():
90121
if 'Token' not in store:
91122
init_store()
92123
tf.close()
124+
if 'captcha_token' not in store:
125+
store['captcha_token'] = ''
93126
except FileNotFoundError:
94-
_error("load_store: store file not found, new authentication required")
127+
_warn("load_store: store file not found, new authentication required")
95128
store = {}
96129
init_store()
97130
except Exception as e:
@@ -119,6 +152,24 @@ def write_store():
119152
os.system("sudo chmod 0666 " + storeFile)
120153

121154

155+
# write state file
156+
def write_state():
157+
global state
158+
global stateFile
159+
try:
160+
tf = open(stateFile, 'w', encoding='utf-8')
161+
except Exception as e:
162+
_error("write_state_file: Exception " + str(e))
163+
os.system("sudo rm -f " + stateFile)
164+
tf = open(stateFile, 'w', encoding='utf-8')
165+
json.dump(state, tf, indent=4)
166+
tf.close()
167+
try:
168+
os.chmod(stateFile, 0o666)
169+
except Exception as e:
170+
os.system("sudo chmod 0666 " + stateFile)
171+
172+
122173
# ---------------HTTP Function-------------------------------------------
123174
def getHTTP(url: str = '', headers: str = '', cookies: str = '', timeout: int = 30) -> str:
124175
try:
@@ -155,7 +206,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = ''
155206
_error("Connection Timeout")
156207
raise
157208
except:
158-
_error("HTTP Error")
209+
_error("HTTP Error, response=" + str(response))
159210
raise
160211

161212
if response.status_code == 200 or response.status_code == 204:
@@ -164,6 +215,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = ''
164215
return response.headers["location"]
165216
else:
166217
_error('Request failed, StatusCode: ' + str(response.status_code))
218+
_error('Request failed, response.content: ' + str(response.content))
167219
raise RuntimeError
168220

169221

@@ -195,13 +247,12 @@ def authStage1(url: str,
195247
password: str,
196248
code_challenge: str,
197249
state: str,
198-
nonce: str) -> str:
250+
nonce: str,
251+
captcha_token: str) -> str:
199252
global config
200253
try:
201254
headers = {
202-
'Content-Type': CONTENT_TYPE,
203-
'user-agent': USER_AGENT,
204-
'x-user-agent': X_USER_AGENT}
255+
'hcaptchatoken': captcha_token}
205256
data = {
206257
'client_id': config['clientId'],
207258
'response_type': 'code',
@@ -280,7 +331,7 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict:
280331
return token
281332

282333

283-
def requestToken(username: str, password: str) -> dict:
334+
def requestToken(username: str, password: str, captcha_token: str) -> dict:
284335
global config
285336
global method
286337
try:
@@ -295,7 +346,7 @@ def requestToken(username: str, password: str) -> dict:
295346
state = get_random_string(22)
296347
nonce = get_random_string(22)
297348

298-
authcode1 = authStage1(authenticate_url, username, password, code_challenge, state, nonce)
349+
authcode1 = authStage1(authenticate_url, username, password, code_challenge, state, nonce, captcha_token)
299350
authcode2 = authStage2(authenticate_url, authcode1, code_challenge, state, nonce)
300351
token = authStage3(token_url, authcode2, code_verifier)
301352
except:
@@ -365,15 +416,19 @@ def requestData(token: str, vin: str) -> dict:
365416
# ---------------Main Function-------------------------------------------
366417
def main():
367418
global store
419+
global state
368420
global storeFile
369421
global DEBUGLEVEL
422+
global CHARGEPOINT
370423
global method
424+
global stateFile
371425
try:
372426
method = ''
373427
CHARGEPOINT = os.environ.get("CHARGEPOINT", "1")
374428
DEBUGLEVEL = int(os.environ.get("debug", "0"))
375-
RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined")
376-
storeFile = RAMDISKDIR + '/soc_i3_cp' + CHARGEPOINT + '.json'
429+
_debug("DEBUGLEVEL=" + str(DEBUGLEVEL))
430+
OPENWBBASEDIR = os.environ.get("OPENWBBASEDIR", "undefined")
431+
storeFile = OPENWBBASEDIR + '/data/i3/soc_i3_cp' + CHARGEPOINT + '.json'
377432
_debug('storeFile =' + storeFile)
378433

379434
argsStr = base64.b64decode(str(sys.argv[1])).decode('utf-8')
@@ -384,13 +439,14 @@ def main():
384439
vin = str(argsDict["vin"]).upper()
385440
socfile = str(argsDict["socfile"])
386441
meterfile = str(argsDict["meterfile"])
387-
statefile = str(argsDict["statefile"])
442+
stateFile = str(argsDict["statefile"])
443+
captcha_token = str(argsDict["captcha_token"])
388444
except:
389445
_error("Parameters could not be processed")
390446
raise
391447

392448
try:
393-
# try to read store file from ramdisk
449+
# try to read store file
394450
expires_in = -1
395451
load_store()
396452
now = int(time.time())
@@ -403,9 +459,13 @@ def main():
403459
expires_in = store['Token']['expires_in']
404460
expires_at = store['expires_at']
405461
token = store['Token']
462+
_exp_at = datetime.fromtimestamp(expires_at).strftime('%Y-%m-%d %H:%M:%S')
463+
_exp_at2 = datetime.fromtimestamp(expires_at-120).strftime('%Y-%m-%d %H:%M:%S')
464+
_now = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
406465
_debug('main0: expires_in=' + str(expires_in) + ', now=' + str(now) +
407-
', expires_at=' + str(expires_at) + ', diff=' + str(expires_at - now))
408-
if now > expires_at - 120:
466+
', expires_at=' + str(expires_at) + ', diff=' + str(now - (expires_at - 120)))
467+
_debug("expires_at=" + _exp_at + ", now=" + _now + ", expires_at-120=" + _exp_at2)
468+
if now > (expires_at - 120):
409469
_debug('call refreshToken')
410470
token = refreshToken(token['refresh_token'])
411471
if 'expires_in' in token:
@@ -420,30 +480,51 @@ def main():
420480
else:
421481
expires_in = store['Token']['expires_in']
422482

423-
# if refreshToken fails, call requestToken
483+
# if refreshToken fails or token are missing, call requestToken
424484
if expires_in == -1:
425-
_debug('call requestToken')
426-
token = requestToken(username, password)
427-
428-
# compute expires_at and store file in ramdisk
485+
# check if there is a new captcha_token, i.e. different from the one already used
486+
last_captcha_token = store['captcha_token']
487+
_debug("captcha_token = " + captcha_token)
488+
_debug("last_captcha_token = " + last_captcha_token)
489+
# if captcha_token is old, quit with error message
490+
if captcha_token == last_captcha_token and captcha_token != "":
491+
authError("Captcha Token wurde bereits verwendet.")
492+
quit()
493+
# if captcha_token is not defined, quit with error message
494+
elif captcha_token == "" or captcha_token is None:
495+
authError("Captcha Token nicht definiert.")
496+
quit()
497+
else:
498+
# looks like we habe a promising captha_token
499+
_debug('call requestToken with captcha_token: \n' + captcha_token)
500+
try:
501+
# store captcha_token in store to detect reuse
502+
store['captcha_token'] = captcha_token
503+
token = requestToken(username, password, captcha_token)
504+
# compute expires_at and write store file
505+
if 'expires_in' in token:
506+
expires_in = int(token['expires_in'])
507+
expires_at = now + expires_in
508+
store['expires_at'] = expires_at
509+
store['Token'] = token
510+
write_store()
511+
_debug('main: token=\n' + json.dumps(token, indent=4))
512+
else:
513+
_error("requestToken failed")
514+
store['expires_at'] = 0
515+
store['Token'] = token
516+
write_store()
517+
except Exception as e:
518+
authError("requestToken Exception: " + str(e))
519+
raise
520+
521+
# get Data from Server
429522
if 'expires_in' in token:
430-
if expires_in != int(token['expires_in']):
431-
expires_in = int(token['expires_in'])
432-
expires_at = now + expires_in
433-
store['expires_at'] = expires_at
434-
store['Token'] = token
435-
write_store()
436-
else:
437-
_error("requestToken failed")
438-
store['expires_at'] = 0
439-
store['Token'] = token
440-
write_store()
441-
_debug('main: token=\n' + json.dumps(token, indent=4))
442-
data = requestData(token, vin)
443-
soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"])
444-
_info("Successful - SoC: " + str(soc) + "%" + ', method=' + method)
445-
except:
446-
_error("Request failed")
523+
data = requestData(token, vin)
524+
soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"])
525+
_info("Successful - SoC: " + str(soc) + "%" + ', method=' + method)
526+
except Exception as e:
527+
_error("Request failed, exception=" + str(e))
447528
raise
448529

449530
try:
@@ -453,8 +534,7 @@ def main():
453534
state["soc"] = int(soc)
454535
with open(meterfile, 'r') as f:
455536
state["meter"] = float(f.read())
456-
with open(statefile, 'w') as f:
457-
f.write(json.dumps(state))
537+
write_state()
458538
except:
459539
_error("Saving SoC failed")
460540
raise

modules/soc_i3/main.sh

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
#!/bin/bash
2+
3+
# -- start user pi enforcement
4+
# normally the soc module runs as user pi
5+
# when LP Configuration is stored, it is run as user www-data
6+
# This leads to various permission problems
7+
# if actual user is not pi, this restarts the script as user pi
8+
usr=`id -nu`
9+
if [ "$usr" != "pi" ]
10+
then
11+
sudo -u pi -c bash "$0 $*"
12+
exit $?
13+
fi
14+
# -- ending user pi enforcement
15+
216
export OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd)
317
export RAMDISKDIR="$OPENWBBASEDIR/ramdisk"
418
export MODULEDIR=$(cd "$(dirname "$0")" && pwd)
@@ -31,6 +45,7 @@ case $CHARGEPOINT in
3145
user=$i3usernames1
3246
pass=$i3passworts1
3347
vin=$i3vins1
48+
captcha_token=$i3captcha_tokens1
3449
;;
3550
*)
3651
# defaults to first charge point for backward compatibility
@@ -47,9 +62,31 @@ case $CHARGEPOINT in
4762
user=$i3username
4863
pass=$i3passwort
4964
vin=$i3vin
65+
captcha_token=$i3captcha_token
5066
;;
5167
esac
5268

69+
# make sure folder data/i3 exists in openwb home folder
70+
# can be executed by pi or www-data so we have to use sudo
71+
prepare_i3DataFolder(){
72+
dataFolder="${OPENWBBASEDIR}/data"
73+
i3Folder="${dataFolder}/i3"
74+
if [ ! -d $i3Folder ]
75+
then
76+
sudo mkdir -p $i3Folder
77+
f=soc_i3_cp1.json
78+
if [ -f $RAMDISKDIR/$f && !-f $i3Folder/$f ]; then
79+
cp $RAMDISKDIR/$f $i3Folder
80+
fi
81+
f=soc_i3_cp2.json
82+
if [ -f $RAMDISKDIR/$f && !-f $i3Folder/$f ]; then
83+
cp $RAMDISKDIR/$f $i3Folder
84+
fi
85+
fi
86+
sudo chown -R pi:pi $dataFolder
87+
sudo chmod 0777 $i3Folder
88+
}
89+
5390
incrementTimer(){
5491
case $dspeed in
5592
1)
@@ -73,12 +110,13 @@ incrementTimer(){
73110
echo $soctimer > "$soctimerfile"
74111
}
75112

113+
prepare_i3DataFolder
76114
soctimer=$(<"$soctimerfile")
77-
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: timer = $soctimer"
115+
openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: timer = $soctimer"
78116
cd $MODULEDIR
79117
if (( soctimer < (6 * intervall) )); then
80118
if(( soccalc < 1 )); then
81-
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: Nothing to do yet. Incrementing timer."
119+
openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: Nothing to do yet. Incrementing timer."
82120
else
83121
ARGS='{'
84122
ARGS+='"socfile": "'"$socfile"'", '
@@ -108,6 +146,7 @@ else
108146
ARGS+='"socfile": "'"$socfile"'", '
109147
ARGS+='"meterfile": "'"$meterfile"'", '
110148
ARGS+='"statefile": "'"$statefile"'", '
149+
ARGS+='"captcha_token": "'"$captcha_token"'", '
111150
ARGS+='"debugLevel": "'"$DEBUGLEVEL"'"'
112151
ARGS+='}'
113152

runs/updateConfig.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,12 @@ updateConfig(){
329329
if ! grep -Fq "i3vin=" $ConfigFile; then
330330
echo "i3vin=VIN" >> $ConfigFile
331331
fi
332+
if ! grep -Fq "i3captcha_token=" $ConfigFile; then
333+
echo "i3captcha_token=''" >> $ConfigFile
334+
fi
335+
if ! grep -Fq "i3captcha_tokens1=" $ConfigFile; then
336+
echo "i3captcha_tokens1=''" >> $ConfigFile
337+
fi
332338
if ! grep -Fq "i3_soccalclp1=" $ConfigFile; then
333339
echo "i3_soccalclp1=0" >> $ConfigFile
334340
fi

0 commit comments

Comments
 (0)