6
6
import sys
7
7
import os
8
8
import time
9
+ from datetime import datetime
9
10
import urllib
10
11
import uuid
11
12
import hashlib
36
37
37
38
# ---------------Helper Function-------------------------------------------
38
39
40
+ def _print (txt : str ):
41
+ ts = datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )
42
+ print (ts + ': ' + txt )
43
+
44
+
39
45
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 )
41
53
42
54
43
55
def _info (txt : str ):
44
56
global DEBUGLEVEL
57
+ global CHARGEPOINT
45
58
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 )
47
65
48
66
49
67
def _debug (txt : str ):
50
68
global DEBUGLEVEL
69
+ global CHARGEPOINT
51
70
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 ("-------------------------------------------------------------------------------------" )
53
83
54
84
55
85
def get_random_string (length : int ) -> str :
@@ -78,6 +108,7 @@ def init_store():
78
108
store = {}
79
109
store ['Token' ] = {}
80
110
store ['expires_at' ] = int (0 )
111
+ store ['captcha_token' ] = ''
81
112
82
113
83
114
# load store from file, initialize store structure if no file exists
@@ -90,8 +121,10 @@ def load_store():
90
121
if 'Token' not in store :
91
122
init_store ()
92
123
tf .close ()
124
+ if 'captcha_token' not in store :
125
+ store ['captcha_token' ] = ''
93
126
except FileNotFoundError :
94
- _error ("load_store: store file not found, new authentication required" )
127
+ _warn ("load_store: store file not found, new authentication required" )
95
128
store = {}
96
129
init_store ()
97
130
except Exception as e :
@@ -119,6 +152,24 @@ def write_store():
119
152
os .system ("sudo chmod 0666 " + storeFile )
120
153
121
154
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
+
122
173
# ---------------HTTP Function-------------------------------------------
123
174
def getHTTP (url : str = '' , headers : str = '' , cookies : str = '' , timeout : int = 30 ) -> str :
124
175
try :
@@ -155,7 +206,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = ''
155
206
_error ("Connection Timeout" )
156
207
raise
157
208
except :
158
- _error ("HTTP Error" )
209
+ _error ("HTTP Error, response=" + str ( response ) )
159
210
raise
160
211
161
212
if response .status_code == 200 or response .status_code == 204 :
@@ -164,6 +215,7 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = ''
164
215
return response .headers ["location" ]
165
216
else :
166
217
_error ('Request failed, StatusCode: ' + str (response .status_code ))
218
+ _error ('Request failed, response.content: ' + str (response .content ))
167
219
raise RuntimeError
168
220
169
221
@@ -195,13 +247,12 @@ def authStage1(url: str,
195
247
password : str ,
196
248
code_challenge : str ,
197
249
state : str ,
198
- nonce : str ) -> str :
250
+ nonce : str ,
251
+ captcha_token : str ) -> str :
199
252
global config
200
253
try :
201
254
headers = {
202
- 'Content-Type' : CONTENT_TYPE ,
203
- 'user-agent' : USER_AGENT ,
204
- 'x-user-agent' : X_USER_AGENT }
255
+ 'hcaptchatoken' : captcha_token }
205
256
data = {
206
257
'client_id' : config ['clientId' ],
207
258
'response_type' : 'code' ,
@@ -280,7 +331,7 @@ def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict:
280
331
return token
281
332
282
333
283
- def requestToken (username : str , password : str ) -> dict :
334
+ def requestToken (username : str , password : str , captcha_token : str ) -> dict :
284
335
global config
285
336
global method
286
337
try :
@@ -295,7 +346,7 @@ def requestToken(username: str, password: str) -> dict:
295
346
state = get_random_string (22 )
296
347
nonce = get_random_string (22 )
297
348
298
- authcode1 = authStage1 (authenticate_url , username , password , code_challenge , state , nonce )
349
+ authcode1 = authStage1 (authenticate_url , username , password , code_challenge , state , nonce , captcha_token )
299
350
authcode2 = authStage2 (authenticate_url , authcode1 , code_challenge , state , nonce )
300
351
token = authStage3 (token_url , authcode2 , code_verifier )
301
352
except :
@@ -365,15 +416,19 @@ def requestData(token: str, vin: str) -> dict:
365
416
# ---------------Main Function-------------------------------------------
366
417
def main ():
367
418
global store
419
+ global state
368
420
global storeFile
369
421
global DEBUGLEVEL
422
+ global CHARGEPOINT
370
423
global method
424
+ global stateFile
371
425
try :
372
426
method = ''
373
427
CHARGEPOINT = os .environ .get ("CHARGEPOINT" , "1" )
374
428
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'
377
432
_debug ('storeFile =' + storeFile )
378
433
379
434
argsStr = base64 .b64decode (str (sys .argv [1 ])).decode ('utf-8' )
@@ -384,13 +439,14 @@ def main():
384
439
vin = str (argsDict ["vin" ]).upper ()
385
440
socfile = str (argsDict ["socfile" ])
386
441
meterfile = str (argsDict ["meterfile" ])
387
- statefile = str (argsDict ["statefile" ])
442
+ stateFile = str (argsDict ["statefile" ])
443
+ captcha_token = str (argsDict ["captcha_token" ])
388
444
except :
389
445
_error ("Parameters could not be processed" )
390
446
raise
391
447
392
448
try :
393
- # try to read store file from ramdisk
449
+ # try to read store file
394
450
expires_in = - 1
395
451
load_store ()
396
452
now = int (time .time ())
@@ -403,9 +459,13 @@ def main():
403
459
expires_in = store ['Token' ]['expires_in' ]
404
460
expires_at = store ['expires_at' ]
405
461
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' )
406
465
_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 ):
409
469
_debug ('call refreshToken' )
410
470
token = refreshToken (token ['refresh_token' ])
411
471
if 'expires_in' in token :
@@ -420,30 +480,51 @@ def main():
420
480
else :
421
481
expires_in = store ['Token' ]['expires_in' ]
422
482
423
- # if refreshToken fails, call requestToken
483
+ # if refreshToken fails or token are missing , call requestToken
424
484
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
429
522
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 ))
447
528
raise
448
529
449
530
try :
@@ -453,8 +534,7 @@ def main():
453
534
state ["soc" ] = int (soc )
454
535
with open (meterfile , 'r' ) as f :
455
536
state ["meter" ] = float (f .read ())
456
- with open (statefile , 'w' ) as f :
457
- f .write (json .dumps (state ))
537
+ write_state ()
458
538
except :
459
539
_error ("Saving SoC failed" )
460
540
raise
0 commit comments