diff --git a/.github/workflows/github-actions-python.yml b/.github/workflows/github-actions-python.yml index 59e6f8c0b..556bffc44 100644 --- a/.github/workflows/github-actions-python.yml +++ b/.github/workflows/github-actions-python.yml @@ -13,6 +13,8 @@ jobs: uses: actions/setup-python@v3 with: python-version: "3.5" + env: + PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/ladelog.sh b/ladelog.sh index 060788403..34c78069d 100755 --- a/ladelog.sh +++ b/ladelog.sh @@ -30,7 +30,7 @@ fi getTimeDiffString() { minutes=$1 - if ((minutes > 60)); then + if ((minutes >= 60)); then text="$((minutes / 60)) H $((minutes % 60)) Min" elif ((minutes >= 0)); then text="$minutes Min" @@ -122,7 +122,7 @@ processChargepoint() { # calculate actual meter value difference since plugged pluggedladungbishergeladen=$(echo "scale=2;($llkwh - $pluggedladungstartkwh)/1" | bc | sed 's/^\./0./') echo "$pluggedladungbishergeladen" >"${RAMDISKDIR}/pluggedladungbishergeladen${chargePointKey}" - openwbDebugLog "CHARGESTAT" 1 "charged since plugged: $pluggedladungstartkwh - $llkwh = $pluggedladungbishergeladen" + openwbDebugLog "CHARGESTAT" 1 "charged since plugged: $llkwh - $pluggedladungstartkwh = $pluggedladungbishergeladen" # reset unplug timer echo 0 >"${RAMDISKDIR}/pluggedtimer${chargePointKey}" else @@ -171,7 +171,7 @@ processChargepoint() { # format time charged restzeittext=$(getTimeDiffString "$restzeit") echo "$restzeittext" >"${RAMDISKDIR}/restzeit${chargePointKey}" - openwbDebugLog "CHARGESTAT" 1 "energyCharged=${bishergeladen}kWh; rangeCharged=${gelr}km; timeRemaining=${restzeit}m ($restzeittext)" + openwbDebugLog "CHARGESTAT" 1 "energyCharged: ${llkwh} - ${ladelstart}=${bishergeladen}kWh; rangeCharged=${gelr}km; timeRemaining=${restzeit}m ($restzeittext)" else # new charge detected openwbDebugLog "CHARGESTAT" 1 "new charge detected" diff --git a/loadvars.sh b/loadvars.sh index 129331c07..3ba4f54ae 100755 --- a/loadvars.sh +++ b/loadvars.sh @@ -1092,7 +1092,7 @@ loadvars(){ fi echo $hausverbrauch > /var/www/html/openWB/ramdisk/hausverbrauch usesimbezug=0 - if [[ $wattbezugmodul == "bezug_solarwatt" ]]|| [[ $wattbezugmodul == "bezug_rct" ]]|| [[ $wattbezugmodul == "bezug_kostalplenticoreem300haus" ]] || [[ $wattbezugmodul == "bezug_solarlog" ]] ; then + if [[ $wattbezugmodul == "bezug_rct" ]]|| [[ $wattbezugmodul == "bezug_kostalplenticoreem300haus" ]] ; then usesimbezug=1 fi if ((usesimbezug == 1)); then @@ -1134,7 +1134,7 @@ loadvars(){ if [[ $speichermodul == "speicher_kostalplenticore" ]] && [[ $pvwattmodul == "wr_plenticore" ]]; then usesimpv=1 fi - if [[ $pvwattmodul == "wr_rct" ]]|| [[ $pvwattmodul == "wr_solarwatt" ]] || [[ $pvwattmodul == "wr_kostalpikovar2" ]]; then + if [[ $pvwattmodul == "wr_rct" ]] || [[ $pvwattmodul == "wr_kostalpikovar2" ]] ; then usesimpv=1 fi if ((usesimpv == 1)); then @@ -1182,7 +1182,7 @@ loadvars(){ echo "$pvallwh" > /var/www/html/openWB/ramdisk/pvallwh fi - if [[ $speichermodul == "speicher_tesvoltsma" ]] || [[ $speichermodul == "speicher_solarwatt" ]] || [[ $speichermodul == "speicher_rct" ]]|| [[ $speichermodul == "speicher_kostalplenticore" ]] ; then + if [[ $speichermodul == "speicher_tesvoltsma" ]] || [[ $speichermodul == "speicher_rct" ]] || [[ $speichermodul == "speicher_kostalplenticore" ]] ; then ra='^-?[0-9]+$' watt2=$(/dev/null else - bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "counter" "$speicher1_ip" "$sungrowspeicherid" "$sungrowsr" >>"$MYLOGFILE" 2>&1 + bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "counter" "$speicher1_ip" "$sungrowspeicherport" "$sungrowspeicherid" "$sungrowsr" >>"$MYLOGFILE" 2>&1 ret=$? fi diff --git a/modules/owbpro/main.sh b/modules/owbpro/main.sh index 80de34ad6..586e6d9fe 100755 --- a/modules/owbpro/main.sh +++ b/modules/owbpro/main.sh @@ -5,16 +5,19 @@ answer=$(timeout 4 curl -s $ip/connect.php | jq .) if [[ $answer == *"vehicle_id"* ]]; then watt=$(echo $answer |jq '.power_all') watt=$(echo $watt | sed 's/\..*$//') - APhase1=$(echo $answer | jq ".currents[0]" ) - APhase2=$(echo $answer | jq ".currents[1]" ) - APhase3=$(echo $answer | jq ".currents[2]" ) + APhase1=$(echo $answer | jq ".currents[0]" ) + APhase2=$(echo $answer | jq ".currents[1]" ) + APhase3=$(echo $answer | jq ".currents[2]" ) + VPhase1=$(echo $answer | jq ".voltages[0]" ) + VPhase2=$(echo $answer | jq ".voltages[1]" ) + VPhase3=$(echo $answer | jq ".voltages[2]" ) boolChargeStat=$(echo $answer | jq ".charge_state" ) if [ $boolChargeStat = true ]; then boolChargeStat=1 else boolChargeStat=0 fi - boolPlugStat=$(echo $answer | jq ".plug_state") + boolPlugStat=$(echo $answer | jq ".plug_state") if [ $boolPlugStat = true ]; then boolPlugStat=1 else @@ -28,6 +31,9 @@ if [[ $answer == *"vehicle_id"* ]]; then echo $APhase1 > /var/www/html/openWB/ramdisk/lla1 echo $APhase2 > /var/www/html/openWB/ramdisk/lla2 echo $APhase3 > /var/www/html/openWB/ramdisk/lla3 + echo $VPhase1 > /var/www/html/openWB/ramdisk/llv1 + echo $VPhase2 > /var/www/html/openWB/ramdisk/llv2 + echo $VPhase3 > /var/www/html/openWB/ramdisk/llv3 echo $watt > /var/www/html/openWB/ramdisk/llaktuell echo $kWhCounter > /var/www/html/openWB/ramdisk/llkwh echo $boolPlugStat > /var/www/html/openWB/ramdisk/plugstat @@ -37,6 +43,9 @@ if [[ $answer == *"vehicle_id"* ]]; then echo $APhase1 > /var/www/html/openWB/ramdisk/llas11 echo $APhase2 > /var/www/html/openWB/ramdisk/llas12 echo $APhase3 > /var/www/html/openWB/ramdisk/llas13 + echo $VPhase1 > /var/www/html/openWB/ramdisk/llvs11 + echo $VPhase2 > /var/www/html/openWB/ramdisk/llvs12 + echo $VPhase3 > /var/www/html/openWB/ramdisk/llvs13 echo $watt > /var/www/html/openWB/ramdisk/llaktuells1 echo $kWhCounter > /var/www/html/openWB/ramdisk/llkwhs1 echo $boolPlugStat > /var/www/html/openWB/ramdisk/plugstats1 @@ -46,6 +55,9 @@ if [[ $answer == *"vehicle_id"* ]]; then echo $APhase1 > /var/www/html/openWB/ramdisk/llas21 echo $APhase2 > /var/www/html/openWB/ramdisk/llas22 echo $APhase3 > /var/www/html/openWB/ramdisk/llas23 + echo $VPhase1 > /var/www/html/openWB/ramdisk/llvs21 + echo $VPhase2 > /var/www/html/openWB/ramdisk/llvs22 + echo $VPhase3 > /var/www/html/openWB/ramdisk/llvs23 echo $watt > /var/www/html/openWB/ramdisk/llaktuells2 echo $kWhCounter > /var/www/html/openWB/ramdisk/llkwhs2 echo $boolPlugStat > /var/www/html/openWB/ramdisk/plugstatlp3 @@ -55,6 +67,9 @@ if [[ $answer == *"vehicle_id"* ]]; then echo $APhase1 > /var/www/html/openWB/ramdisk/lla1lp$chargep echo $APhase2 > /var/www/html/openWB/ramdisk/lla2lp$chargep echo $APhase3 > /var/www/html/openWB/ramdisk/lla3lp$chargep + echo $VPhase1 > /var/www/html/openWB/ramdisk/llv1lp$chargep + echo $VPhase2 > /var/www/html/openWB/ramdisk/llv2lp$chargep + echo $VPhase3 > /var/www/html/openWB/ramdisk/llv3lp$chargep echo $watt > /var/www/html/openWB/ramdisk/llaktuelllp$chargep echo $kWhCounter > /var/www/html/openWB/ramdisk/llkwhlp$chargep echo $boolPlugStat > /var/www/html/openWB/ramdisk/plugstatlp$chargep diff --git a/modules/soc_evcc/soc b/modules/soc_evcc/soc index ba92422f7..11f0e0bfa 100755 Binary files a/modules/soc_evcc/soc and b/modules/soc_evcc/soc differ diff --git a/modules/soc_http/main.sh b/modules/soc_http/main.sh index 53886a349..ba0350d07 100755 --- a/modules/soc_http/main.sh +++ b/modules/soc_http/main.sh @@ -64,7 +64,7 @@ getAndWriteSoc(){ re='^-?[0-9]+$' openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: Requesting SoC" echo 0 > "$soctimerfile" - soc=$(curl --connect-timeout 15 -s "$ip" | cut -f1 -d".") + soc=$(curl --connect-timeout 15 -s -g "$ip" | cut -f1 -d".") if [[ $soc =~ $re ]] ; then if (( soc != 0 )) ; then diff --git a/modules/soc_i3/i3soc.py b/modules/soc_i3/i3soc.py index d6b8782cd..8279b5271 100755 --- a/modules/soc_i3/i3soc.py +++ b/modules/soc_i3/i3soc.py @@ -4,18 +4,84 @@ import requests import string import sys +import os import time +from datetime import datetime import urllib +import uuid +import hashlib + # ---------------Constants------------------------------------------- auth_server = 'customer.bmwgroup.com' api_server = 'cocoapi.bmwgroup.com' +APIKey = b'NGYxYzg1YTMtNzU4Zi1hMzdkLWJiYjYtZjg3MDQ0OTRhY2Zh' +USER_AGENT = 'Dart/3.3 (dart:io)' +REGION = '0' # 0 = rest_of_world +BRAND = 'bmw' # for auth bmw or mini don't matter +X_USER_AGENT1 = 'android(AP2A.240605.024);' +X_USER_AGENT2 = ';4.7.2(35379);' +X_USER_AGENT = X_USER_AGENT1 + BRAND + X_USER_AGENT2 + REGION +CONTENT_TYPE = 'application/x-www-form-urlencoded' +CHARSET = 'charset=UTF-8' + +storeFile = 'i3soc.json' + -client_id = '31c357a0-7a1d-4590-aa99-33b97244d048' -client_password = 'c0e3393d-70a2-4f6f-9d3c-8530af64d552' +# --------------- Global Variables -------------------------------------- +store = {} +config = {} +DEBUGLEVEL = 0 +method = '' # ---------------Helper Function------------------------------------------- + +def _print(txt: str): + ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(ts + ': ' + txt) + + +def _error(txt: str): + 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("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("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: letters = string.ascii_letters result_str = ''.join(random.choice(letters) for i in range(length)) @@ -30,33 +96,118 @@ def create_auth_string(client_id: str, client_password: str) -> str: return authstring +def create_s256_code_challenge(code_verifier: str) -> str: + """Create S256 code_challenge with the given code_verifier.""" + data = hashlib.sha256(code_verifier.encode("ascii")).digest() + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("UTF-8") + + +# initialize store structures when no store is available +def init_store(): + global store + store = {} + store['Token'] = {} + store['expires_at'] = int(0) + store['captcha_token'] = '' + store['session_id'] = str(uuid.uuid4()) + + +# load store from file, initialize store structure if no file exists +def load_store(): + global store + global storeFile + try: + tf = open(storeFile, 'r', encoding='utf-8') + store = json.load(tf) + if 'Token' not in store: + init_store() + tf.close() + if 'captcha_token' not in store: + store['captcha_token'] = '' + except FileNotFoundError: + _warn("load_store: store file not found, new authentication required") + store = {} + init_store() + except Exception as e: + _error("init: loading stored data failed, file: " + + storeFile + ", error=" + str(e)) + store = {} + init_store() + + +# write store file +def write_store(): + global store + global storeFile + try: + tf = open(storeFile, 'w', encoding='utf-8') + except Exception as e: + _error("write_store_file: Exception " + str(e)) + os.system("sudo rm -f " + storeFile) + tf = open(storeFile, 'w', encoding='utf-8') + json.dump(store, tf, indent=4) + tf.close() + try: + os.chmod(storeFile, 0o666) + except Exception as e: + 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: response = requests.get(url, headers=headers, cookies=cookies, timeout=timeout) except requests.Timeout: - print("Connection Timeout") + _error("Connection Timeout") raise - except: - print("HTTP Error") + except Exception as e: + _error("HTTP Error:" + str(e)) raise if response.status_code == 200 or response.status_code == 204: return response.text else: - print('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, StatusCode: ' + str(response.status_code)) raise RuntimeError -def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '', timeout: int = 30, allow_redirects: bool = True) -> str: +def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '', + timeout: int = 30, allow_redirects: bool = True, + authId: str = '', authSec: str = '') -> str: try: - response = requests.post(url, data=data, headers=headers, cookies=cookies, - timeout=timeout, allow_redirects=allow_redirects) + _debug("postHTTP: url=" + url + + ",\nheaders=" + json.dumps(headers, indent=4) + + ",\ndata=" + json.dumps(data, indent=4)) + if authId != '': + response = requests.post(url, data=data, headers=headers, cookies=cookies, + timeout=timeout, auth=(authId, authSec), + allow_redirects=allow_redirects) + else: + response = requests.post(url, data=data, headers=headers, cookies=cookies, + timeout=timeout, allow_redirects=allow_redirects) except requests.Timeout: - print("Connection Timeout") + _error("Connection Timeout") raise except: - print("HTTP Error") + _error("HTTP Error, response=" + str(response)) raise if response.status_code == 200 or response.status_code == 204: @@ -64,166 +215,340 @@ def postHTTP(url: str = '', data: str = '', headers: str = '', cookies: str = '' elif response.status_code == 302: return response.headers["location"] else: - print('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, StatusCode: ' + str(response.status_code)) + _error('Request failed, response.content: ' + str(response.content)) raise RuntimeError +def authStage0() -> str: + global session_id + try: + if not session_id: + session_id = str(uuid.uuid4()) + id1 = str(uuid.uuid4()) + ocp = base64.b64decode(APIKey).decode() + url = 'https://' + api_server + '/eadrax-ucs/v1/presentation/oauth/config' + headers = { + 'ocp-apim-subscription-key': ocp, + 'bmw-session-id': session_id, + 'x-identity-provider': 'gcdm', + 'x-correlation-id': id1, + 'bmw-correlation-Id': id1, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} + body = getHTTP(url, headers) + cfg = json.loads(body) + except: + _error("authStage0 failed") + raise + return cfg + + # ---------------Authentication Function------------------------------------------- -def authStage1(username: str, password: str, code_challenge: str, state: str) -> str: +def authStage1(url: str, + username: str, + password: str, + code_challenge: str, + state: str, + nonce: str, + captcha_token: str) -> str: + global config try: - url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'} + 'hcaptchatoken': captcha_token} data = { - 'client_id': client_id, + 'client_id': config['clientId'], 'response_type': 'code', - 'scope': 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', - 'redirect_uri': 'com.bmw.connected://oauth', + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], 'state': state, - 'nonce': 'login_nonce', + 'nonce': nonce, 'code_challenge': code_challenge, - 'code_challenge_method': 'plain', + 'code_challenge_method': 'S256', 'username': username, 'password': password, 'grant_type': 'authorization_code'} - - response = json.loads(postHTTP(url, data, headers)) + + resp = postHTTP(url, data, headers) + response = json.loads(resp) authcode = dict(urllib.parse.parse_qsl(response["redirect_to"]))["authorization"] + _debug("authStage1: authcode=" + authcode) except: - print("Authentication stage 1 failed") + _error("Authentication stage 1 failed") raise - + return authcode -def authStage2(authcode1: str, code_challenge: str, state: str) -> str: +def authStage2(url: str, authcode1: str, code_challenge: str, state: str, nonce: str) -> str: try: - url = 'https://' + auth_server + '/gcdm/oauth/authenticate' headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'} + 'Content-Type': CONTENT_TYPE, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} data = { - 'client_id': client_id, + 'client_id': config['clientId'], 'response_type': 'code', - 'scope': 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', - 'redirect_uri': 'com.bmw.connected://oauth', + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], 'state': state, - 'nonce': 'login_nonce', + 'nonce': nonce, 'code_challenge': code_challenge, - 'code_challenge_method': 'plain', + 'code_challenge_method': 'S256', 'authorization': authcode1} cookies = { 'GCDMSSO': authcode1} - + response = postHTTP(url, data, headers, cookies, allow_redirects=False) - authcode = dict(urllib.parse.parse_qsl(response.split("?",1)[1]))["code"] + _debug("authStage2: response=" + response) + authcode = dict(urllib.parse.parse_qsl(response.split("?", 1)[1]))["code"] + _debug("authStage2: authcode=" + authcode) except: - print("Authentication stage 2 failed") + _error("Authentication stage 2 failed") raise - + return authcode -def authStage3(authcode2: str, code_challenge: str) -> dict: +def authStage3(token_url: str, authcode2: str, code_verifier: str) -> dict: + global config try: - url = 'https://' + auth_server + '/gcdm/oauth/token' + url = token_url headers = { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - 'Authorization': create_auth_string(client_id, client_password)} + 'Content-Type': CONTENT_TYPE + '; ' + CHARSET} data = { 'code': authcode2, - 'code_verifier': code_challenge, - 'redirect_uri': 'com.bmw.connected://oauth', + 'code_verifier': code_verifier, + 'redirect_uri': config['returnUrl'], 'grant_type': 'authorization_code'} - - response = postHTTP(url, data, headers, allow_redirects=False) + authId = config['clientId'] + authSec = config['clientSecret'] + response = postHTTP(url, data, headers, authId=authId, authSec=authSec, allow_redirects=False) + _debug("authStage3: response=" + response) token = json.loads(response) + _debug("authStage3: token=" + json.dumps(token, indent=4)) except: - print("Authentication stage 3 failed") + _error("Authentication stage 3 failed") raise - + return token -def requestToken(username: str, password: str) -> dict: +def requestToken(username: str, password: str, captcha_token: str) -> dict: + global config + global method try: - code_challenge = get_random_string(86) + # new: get oauth config from server + method += ' requestToken' + config = authStage0() + _debug('config=\n' + json.dumps(config, indent=4)) + token_url = config['tokenEndpoint'] + authenticate_url = token_url.replace('/token', '/authenticate') + code_verifier = get_random_string(86) + code_challenge = create_s256_code_challenge(code_verifier) state = get_random_string(22) - - authcode1 = authStage1(username, password, code_challenge, state) - authcode2 = authStage2(authcode1, code_challenge, state) - token = authStage3(authcode2, code_challenge) + nonce = get_random_string(22) + + 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: - print("Login failed") + _error("Login failed") raise - + + return token + + +def refreshToken(refreshToken: str) -> dict: + global config + global method + try: + method += ' refreshToken' + config = authStage0() + url = config['tokenEndpoint'] + headers = { + 'Content-Type': CONTENT_TYPE, + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT} + data = { + 'scope': ' '.join(config['scopes']), + 'redirect_uri': config['returnUrl'], + 'grant_type': 'refresh_token', + 'refresh_token': refreshToken} + authId = config['clientId'] + authSec = config['clientSecret'] + resp = postHTTP(url, data, headers, authId=authId, authSec=authSec, allow_redirects=False) + token = json.loads(resp) + except: + _error("Login failed") + raise + return token # ---------------Interface Function------------------------------------------- def requestData(token: str, vin: str) -> dict: + global method try: + method += ' requestData' if vin[:2] == 'WB': brand = 'bmw' elif vin[:2] == 'WM': brand = 'mini' else: - print("Unknown VIN") + _error("Unknown VIN") raise RuntimeError - + url = 'https://' + api_server + '/eadrax-vcs/v4/vehicles/state' headers = { - 'User-Agent': 'Dart/2.14 (dart:io)', - 'x-user-agent': 'android(SP1A.210812.016.C1);' + brand + ';2.5.2(14945);row', + 'user-agent': USER_AGENT, + 'x-user-agent': X_USER_AGENT1 + brand + X_USER_AGENT2 + REGION, 'bmw-vin': vin, 'Authorization': (token["token_type"] + " " + token["access_token"])} - body = getHTTP(url, headers) + body = getHTTP(url, headers) response = json.loads(body) except: - print("Data-Request failed") - raise - + _error("Data-Request failed") + _error("requestData: url=" + url + + ",\nheaders=" + json.dumps(headers, indent=4)) + raise + return response # ---------------Main Function------------------------------------------- def main(): + global store + global state + global storeFile + global DEBUGLEVEL + global CHARGEPOINT + global method + global stateFile + global session_id try: + method = '' + CHARGEPOINT = os.environ.get("CHARGEPOINT", "1") + DEBUGLEVEL = int(os.environ.get("debug", "0")) + _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') argsDict = json.loads(argsStr) - + username = str(argsDict["user"]) password = str(argsDict["pass"]) 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: - print("Parameters could not be processed") + _error("Parameters could not be processed") raise - + + try: + # try to read store file + expires_in = -1 + load_store() + now = int(time.time()) + _debug('main0: store=\n' + json.dumps(store, indent=4)) + if 'session_id' not in store: + store['session_id'] = str(uuid.uuid4()) + session_id = store['session_id'] + # if OK, check if refreshToken is required + if 'expires_at' in store and \ + 'Token' in store and \ + 'expires_in' in store['Token'] and \ + 'refresh_token' in store['Token']: + 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(now - (expires_at - 120))) + _debug("expires_at=" + _exp_at + ", now=" + _now + ", expires_at-120=" + _exp_at2) + if now > (expires_at - 120): + try: + _debug('call refreshToken') + token = refreshToken(token['refresh_token']) + except Exception as e: + _debug("main1: refresh_token failed, err=" + str(e)) + token = {} + 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() + else: + _error("refreshToken failed, re-authenticate") + expires_in = -1 + else: + expires_in = store['Token']['expires_in'] + + # if refreshToken fails or token are missing, call requestToken + if expires_in == -1: + # 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: + 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: - token = requestToken(username, password) - data = requestData(token, vin) - soc = int(data["state"]["electricChargingState"]["chargingLevelPercent"]) - print("Download sucessful - SoC: " + str(soc) + "%") - except: - print("Request failed") - raise - - try: with open(socfile, 'w') as f: f.write(str(int(soc))) state = {} 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: - print("Saving SoC failed") - raise + _error("Saving SoC failed") + raise if __name__ == '__main__': diff --git a/modules/soc_i3/main.sh b/modules/soc_i3/main.sh index 02727d47e..3c40707ee 100755 --- a/modules/soc_i3/main.sh +++ b/modules/soc_i3/main.sh @@ -1,10 +1,24 @@ #!/bin/bash -OPENWBBASEDIR=$(cd "$(dirname "$0")/../../" && pwd) -RAMDISKDIR="$OPENWBBASEDIR/ramdisk" -MODULEDIR=$(cd "$(dirname "$0")" && pwd) + +# -- 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) LOGFILE="$RAMDISKDIR/soc.log" DMOD="EVSOC" -CHARGEPOINT=$1 +export CHARGEPOINT=$1 # check if config file is already in env if [[ -z "$debug" ]]; then @@ -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 -a ! -f $i3Folder/$f ]; then + cp $RAMDISKDIR/$f $i3Folder + fi + f=soc_i3_cp2.json + if [ -f $RAMDISKDIR/$f -a ! -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"'", ' @@ -91,7 +129,7 @@ if (( soctimer < (6 * intervall) )); then ARGSB64=$(echo -n $ARGS | base64 --wrap=0) - sudo python3 "$MODULEDIR/manual.py" "$ARGSB64" &>> $LOGFILE & + python3 "$MODULEDIR/manual.py" "$ARGSB64" &>> $LOGFILE & soclevel=$(<"$socfile") openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: SoC: $soclevel" @@ -108,12 +146,13 @@ else ARGS+='"socfile": "'"$socfile"'", ' ARGS+='"meterfile": "'"$meterfile"'", ' ARGS+='"statefile": "'"$statefile"'", ' + ARGS+='"captcha_token": "'"$captcha_token"'", ' ARGS+='"debugLevel": "'"$DEBUGLEVEL"'"' ARGS+='}' ARGSB64=$(echo -n $ARGS | base64 --wrap=0) - sudo python3 "$MODULEDIR/i3soc.py" "$ARGSB64" &>> $LOGFILE & + python3 "$MODULEDIR/i3soc.py" "$ARGSB64" &>> $LOGFILE & soclevel=$(<"$socfile") openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: SoC: $soclevel" diff --git a/modules/soc_kia/parameters.py b/modules/soc_kia/parameters.py index 8e8cb08c7..9b701e573 100755 --- a/modules/soc_kia/parameters.py +++ b/modules/soc_kia/parameters.py @@ -117,17 +117,17 @@ def loadBrandData(): setParameter('baseUrl', 'https://' + getParameter('host')) setParameter('clientId', 'fdc85c00-0a2f-4c64-bcb4-2cfb1500730a') setParameter('authClientId', '572e0304-5f8d-4b4c-9dd5-41aa84eed160') - setParameter('appId', 'a2b8469b-30a3-4361-8e13-6fceea8fbe74') - setParameter('GCMSenderId', '345127537656') - setParameter('PushType', 'APNS') + setParameter('appId', '1518dd6b-2759-4995-9ae5-c9ad4a9ddad1') + setParameter('GCMSenderId', 'cF5o4DiiQkaw5wsAkLzYIS:APA91bFB59MltBMK29zI0U2llq7khbB2jELkNFKMfBCH6KlCPL16pz_dG0fZ4ncvFn1IMT8nfojb83JyLiT_skBTXtClHhDCKeRbyPy3yQjCVRC3zTZt--wI7vv4jD9aknhHhiQsoZoU') + setParameter('PushType', 'GCM') setParameter('basicToken', 'Basic ZmRjODVjMDAtMGEyZi00YzY0LWJjYjQtMmNmYjE1MDA3MzBhOnNlY3JldA==') if getParameter('brand') == 'hyundai': setParameter('host', 'prd.eu-ccapi.hyundai.com:8080') setParameter('baseUrl', 'https://' + getParameter('host')) setParameter('clientId', '6d477c38-3ca4-4cf3-9557-2a1929a94654') setParameter('authClientId', '64621b96-0f0d-11ec-82a8-0242ac130003') - setParameter('appId', '1eba27d2-9a5b-4eba-8ec7-97eb6c62fb51') - setParameter('GCMSenderId', '414998006775') + setParameter('appId', '014d2225-8495-4735-812d-2616334fd15d') + setParameter('GCMSenderId', 'dQtCqr7gRjy31Ao4nPiLVy:APA91bF_tv9yPOTFa-sW9-vCxOVpzD_iLjRopN_zaKgPKdwS7OYTWFN626-ObhZyzka5kYFKG0KfCsuMOUD5aw9Gyrdh-IeBQZHIcfb5YNUrQBvfqQxbggk9kO6gZeFbCpCLHZB6wITC') setParameter('PushType', 'GCM') setParameter( 'basicToken', 'Basic NmQ0NzdjMzgtM2NhNC00Y2YzLTk1NTctMmExOTI5YTk0NjU0OktVeTQ5WHhQekxwTHVvSzB4aEJDNzdXNlZYaG10UVI5aVFobUlGampvWTRJcHhzVg==') diff --git a/modules/soc_kia/stamps.py b/modules/soc_kia/stamps.py index 792767490..debe10542 100755 --- a/modules/soc_kia/stamps.py +++ b/modules/soc_kia/stamps.py @@ -12,12 +12,12 @@ def getStamp(): # Set App-ID and App-ID specific key brand = parameters.getParameter('brand') if brand == 'kia': - appid = "a2b8469b-30a3-4361-8e13-6fceea8fbe74" - secret_ba = bytearray.fromhex("C0B4D5C7089D987F027C96015929C70FA13486E934A33762BB2801E212E43395C283300BD43939B04DFA77F6F1E4F14C6D9B") + appid = "1518dd6b-2759-4995-9ae5-c9ad4a9ddad1" + secret_ba = bytearray.fromhex("C0B4D5C7089D987F027C96015929C70FA9D2B2AA99530CFD017E4B243C4BA5C5DED96DEB128EEB5DD3963DFC12432C9073EF") if brand == 'hyundai': - appid = "1eba27d2-9a5b-4eba-8ec7-97eb6c62fb51" - secret_ba = bytearray.fromhex("445B6846AFEF0D726646776865A650C9AEF98E51A474DCB7EC9B1B67D29C66EAAEF621CA02522A0B80A8087F7A3A7BB0F71B") + appid = "014d2225-8495-4735-812d-2616334fd15d" + secret_ba = bytearray.fromhex("445B6846AFEF0D726646776865A650C9F3A8B7B3AB22A195163F7A898D962F7CB21F967FA54BE5521AA60B10F6B7E0FADC3B") # Combine plaintext and convert to bytearray plaintext = appid + ":" + now diff --git a/modules/soc_leaf/pycarwings2.py b/modules/soc_leaf/pycarwings2.py index d583d9b9e..442f5e82f 100755 --- a/modules/soc_leaf/pycarwings2.py +++ b/modules/soc_leaf/pycarwings2.py @@ -72,7 +72,7 @@ import base64 from Crypto.Cipher import Blowfish -BASE_URL = "https://gdcportalgw.its-mo.com/api_v210707_NE/gdc/" +BASE_URL = "https://gdcportalgw.its-mo.com/api_v230317_NE/gdc/" log = logging.getLogger(__name__) @@ -118,7 +118,7 @@ def _request(self, endpoint, params): else: params["custom_sessionid"] = "" - req = Request('POST', url=BASE_URL + endpoint, data=params).prepare() + req = Request('POST', url=BASE_URL + endpoint, data=params, headers={"User-Agent": ""}).prepare() log.debug("invoking carwings API: %s" % req.url) log.debug("params: %s" % json.dumps( diff --git a/modules/soc_smarteq/main.sh b/modules/soc_ovms/main.sh similarity index 72% rename from modules/soc_smarteq/main.sh rename to modules/soc_ovms/main.sh index 24d921cc2..5d9d7b27b 100755 --- a/modules/soc_smarteq/main.sh +++ b/modules/soc_ovms/main.sh @@ -8,7 +8,7 @@ export OPENWBBASEDIR RAMDISKDIR MODULEDIR # check if config file is already in env if [[ -z "$debug" ]]; then - echo "soc_smarteq: Seems like openwb.conf is not loaded. Reading file." + echo "soc_ovms: Seems like openwb.conf is not loaded. Reading file." # try to load config . $OPENWBBASEDIR/loadconfig.sh # load helperFunctions @@ -22,9 +22,10 @@ case $CHARGEPOINT in soctimerfile="$RAMDISKDIR/soctimer1" socfile="$RAMDISKDIR/soc1" fztype=$soc2type + server=$soc2server username=$soc2user password=$soc2pass - vin=$soc2vin + vehicleId=$soc2vehicleid intervall=$(( soc2intervall * 6 )) intervallladen=$(( soc2intervallladen * 6 )) ;; @@ -35,11 +36,12 @@ case $CHARGEPOINT in ladeleistung=$(<$RAMDISKDIR/llaktuell) soctimerfile="$RAMDISKDIR/soctimer" socfile="$RAMDISKDIR/soc" - username=$soc_smarteq_username - password=$soc_smarteq_passwort - vin=$soc_smarteq_vin - intervall=$(( soc_smarteq_intervall * 6 )) - intervallladen=$(( soc_smarteq_intervallladen * 6 )) + server=$soc_ovms_server + username=$soc_ovms_username + password=$soc_ovms_passwort + vehicleId=$soc_ovms_vehicleid + intervall=$(( soc_ovms_intervall * 6 )) + intervallladen=$(( soc_ovms_intervallladen * 6 )) ;; esac @@ -69,21 +71,14 @@ incrementTimer(){ getAndWriteSoc(){ openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: Requesting SoC" echo 0 > $soctimerfile - #Prepare for secrets used in soc module libvwid in Python - if ! python3 -c "import secrets" &> /dev/null ; then - if [ ! -L $MODULEDIR/secrets.py ]; then - echo 'soc_vwid: enable local secrets.py...' - ln -s $MODULEDIR/_secrets.py $MODULEDIR/secrets.py - fi - fi - answer=$($MODULEDIR/soc_smarteq.py --user "$username" --password "$password" --vin "$vin" --chargepoint "$CHARGEPOINT" 2>>$RAMDISKDIR/soc.log) + answer=$($MODULEDIR/soc_ovms.py --server "$server" --user "$username" --password "$password" --vehicleId "$vehicleId" --chargepoint "$CHARGEPOINT" 2>>$RAMDISKDIR/soc.log) if [ $? -eq 0 ]; then # we got a valid answer echo $answer > $socfile openwbDebugLog ${DMOD} 2 "Lp$CHARGEPOINT: SoC: $answer" else # we have a problem - openwbDebugLog ${DMOD} 0 "Lp$CHARGEPOINT: Error from soc_smart: $answer" + openwbDebugLog ${DMOD} 0 "Lp$CHARGEPOINT: Error from soc_ovms: $answer" fi } diff --git a/modules/soc_ovms/soc_ovms.py b/modules/soc_ovms/soc_ovms.py new file mode 100755 index 000000000..2d93ed196 --- /dev/null +++ b/modules/soc_ovms/soc_ovms.py @@ -0,0 +1,279 @@ +#!/usr/bin/python3 + +from argparse import ArgumentParser +import requests +import logging +import os +import time +from datetime import datetime +from typing import Union +from json import loads, dumps + +TS_FMT = "%Y-%m-%dT%H:%M:%S" + +# OVMS_SERVER = "https://ovms.dexters-web.de:6869" +TOKEN_CMD = "/api/token" +STATUS_CMD = "/api/status" +OVMS_APPL_LABEL = "application" +OVMS_APPL_VALUE = "owb-ovms-1.9" +OVMS_PURPOSE_LABEL = "purpose" +OVMS_PURPOSE_VALUE = "get soc" + + +def utc2local(utc): + global log, session, token, vehicleId + epoch = time.mktime(utc.timetuple()) + offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) + return utc + offset + + +# sync function +def fetch_soc(id: str, pw: str, chargepoint: int) -> int: + global log, session, token, vehicleId + + # get soc, from server + soc = _fetch_soc(id, pw, chargepoint) + + return soc + + +def read_token_file() -> Union[int, dict]: + global tokenFile + rc = 0 + try: + with open(tokenFile, "r") as f: + jsonstr = f.read() + confDict = loads(jsonstr) + except Exception as e: + log.exception("Token file read exception" + str(e)) + token = "" + confDict = {} + confDict['configuration'] = {} + confDict['configuration']['token'] = token + rc = 1 + return rc, confDict + + +def write_token_file(confDict: dict): + global tokenFile + try: + with open(tokenFile, "w") as f: + jsonstr = dumps(confDict, indent=4) + f.write(jsonstr) + except Exception as e: + log.exception("Token file write exception" + str(e)) + + try: + os.chmod(tokenFile, 0o777) + except Exception as e: + log.exception("chmod tokenFile exception, e="+str(e)) + os.system("sudo chmod 0777 "+tokenFile) + + +def main(): + global log, session, token, vehicleId, tokenFile, OVMS_SERVER + + log = logging.getLogger("soc_ovms") + token = "" + + parser = ArgumentParser() + parser.add_argument("-s", "--server", + help="server", metavar="server", required=True) + parser.add_argument("-u", "--user", + help="user", metavar="user", required=True) + parser.add_argument("-p", "--password", + help="password", metavar="password", required=True) + parser.add_argument("-v", "--vehicleId", + help="vehicleId", metavar="vehicleId", required=True) + parser.add_argument("-c", "--chargepoint", + help="chargepoint", metavar="chargepoint", required=True) + + args = vars(parser.parse_args()) + OVMS_SERVER = args['server'] + id = args['user'] + pw = args['password'] + vehicleId = args['vehicleId'] + chargepoint = args['chargepoint'] + + # logging setup + debug = os.environ.get('debug', '0') + LOGLEVEL = 'WARN' + if debug == '1': + LOGLEVEL = 'INFO' + if debug == '2': + LOGLEVEL = 'DEBUG' + RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined") + logFile = RAMDISKDIR+'/soc.log' + format = '%(asctime)s %(levelname)s:%(name)s:Lp' + str(chargepoint) + ' %(message)s' + datefmt = '%Y-%m-%d %H:%M:%S' + logging.basicConfig(filename=logFile, + filemode='a', + format=format, + datefmt=datefmt, + level=LOGLEVEL) + + log.debug("server=" + OVMS_SERVER + + ", user=" + id + + ", pw=" + pw + + ", vehicleId=" + vehicleId + + ", cp=" + chargepoint) + RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined") + tokenFile = RAMDISKDIR+'/soc_ovms_tokenlp'+chargepoint + + with requests.Session() as session: + soc = fetch_soc(id, pw, chargepoint) + print(str(soc)) + + +# create a new token and store it in the soc_module configuration +def create_token(user_id: str, password: str, chargepoint: int) -> str: + global log, session, token, vehicleId, OVMS_SERVER + token_url = OVMS_SERVER + TOKEN_CMD + appl = OVMS_APPL_VALUE + str(chargepoint) + data = { + "username": user_id, + "password": password + } + form_data = { + OVMS_APPL_LABEL: appl, + OVMS_PURPOSE_LABEL: OVMS_PURPOSE_VALUE + } + try: + resp = session.post(token_url, params=data, files=form_data) + except Exception as e: + resp = e.response + + log.debug("create_token status_code=" + str(resp.status_code)) + tokenDict = loads(resp.text) + log.debug("create_token response=" + dumps(tokenDict, indent=4)) + token = tokenDict['token'] + confDict = {} + confDict['configuration'] = {} + confDict["configuration"]["token"] = resp.text.rstrip() + log.debug("create_token confDict=" + dumps(confDict, indent=4)) + write_token_file(confDict) + + return token + + +# check list of token on OVMS server for unused token created by the soc mudule +# if any obsolete token are found these are deleted. +def cleanup_token(user_id: str, password: str, chargepoint: int): + global log, session, token, vehicleId, OVMS_SERVER + tokenlist_url = OVMS_SERVER + TOKEN_CMD + '?username=' + user_id + '&' + 'password=' + token + + log.debug("tokenlist_url=" + tokenlist_url) + try: + resp = session.get(tokenlist_url) + except Exception as e: + log.error("cleanup_token: exception = " + str(e)) + resp = e.response + + status_code = resp.status_code + if status_code > 299: + log.error("cleanup_token status_code=" + str(status_code)) + full_tokenlist = {} + else: + response = resp.text + full_tokenlist = loads(response) + appl = OVMS_APPL_VALUE + str(chargepoint) + log.debug("cleanup_token status_code=" + + str(status_code) + ", full_tokenlist=\n" + + dumps(full_tokenlist, indent=4)) + obsolete_tokenlist = list(filter(lambda token: + token[OVMS_APPL_LABEL] == appl and token["token"] != token, + full_tokenlist)) + log.debug("cleanup_token: obsolete_tokenlist=\n" + + dumps(obsolete_tokenlist, indent=4)) + if len(obsolete_tokenlist) > 0: + log.debug("cleanup_token obsolete_tokenlist=\n" + dumps(obsolete_tokenlist, indent=4)) + for tok in obsolete_tokenlist: + token_to_delete = tok["token"] + if token_to_delete != token: + log.debug("cleanup_token: token_to_delete=" + dumps(tok, indent=4)) + token_del_url = OVMS_SERVER + TOKEN_CMD + '/' + token_to_delete + token_del_url = token_del_url + '?username=' + user_id + '&password=' + token + log.debug("token_del_url=" + token_del_url) + try: + resp = session.delete(token_del_url) + except Exception as e: + log.error("delete_token: exception = " + str(e)) + resp = e.response + + status_code = resp.status_code + else: + log.debug("cleanup_token: skip active token: " + token) + else: + log.debug("cleanup_token: no obsolete token found") + + return + + +# get status for vehicleId +def get_status(user_id: str) -> Union[int, dict]: + global log, session, token, vehicleId, OVMS_SERVER + status_url = OVMS_SERVER + STATUS_CMD + '/' + vehicleId + '?username=' + user_id + '&password=' + token + + log.debug("status-url=" + status_url) + try: + resp = session.get(status_url) + except Exception as e: + resp = e.response + + status_code = resp.status_code + if status_code > 299: + log.error("get_status status_code=" + str(status_code) + ", create new token") + respDict = {} + else: + response = resp.text + respDict = loads(response) + log.debug("get_status status_code=" + str(status_code) + ", response=" + dumps(respDict, indent=4)) + return int(status_code), respDict + + +# function to fetch soc, range, soc_ts +def _fetch_soc(user_id: str, password: str, chargepoint: int) -> int: + global log, session, token, vehicleId + + try: + rc, confDict = read_token_file() + if rc == 0: + tokenstr = confDict['configuration']['token'] + tokdict = loads(tokenstr) + token = tokdict['token'] + log.debug("read token: " + token) + if token is None or token == "": + token = create_token(user_id, password, chargepoint) + log.debug("create_token: " + token) + except Exception as e: + log.info("_fetch_soc exception:" + str(e) + ", create new token") + token = create_token(user_id, password, chargepoint) + log.debug("create_token: " + token) + + log.debug("call get_status, token:" + token) + status_code, statusDict = get_status(user_id) + if status_code > 299: + token = create_token(user_id, password, chargepoint) + status_code, statusDict = get_status(user_id) + if status_code > 299: + raise "Authentication Problem, status_code " + str(status_code) + + soc = statusDict['soc'] + range = statusDict['estimatedrange'] + kms = statusDict['odometer'] + vehicle12v = statusDict['vehicle12v'] + + soc_ts = statusDict['m_msgtime_s'] + log.info("soc=" + str(soc) + + ", range=" + str(range) + + ", soc_ts=" + str(soc_ts) + + ", km-stand=" + str(float(kms)/10) + + ", soc_12v=" + str(vehicle12v)) + + cleanup_token(user_id, password, chargepoint) + + return int(float(soc)) + + +if __name__ == '__main__': + main() diff --git a/modules/soc_smarteq/README.txt b/modules/soc_smarteq/README.txt deleted file mode 100755 index f3d5404a0..000000000 --- a/modules/soc_smarteq/README.txt +++ /dev/null @@ -1,12 +0,0 @@ -Das smart SOC Modul ist vom smartEQ Modul des iobroker inspiriert. -Der js code wurde in python implementiert und etwas vereinfacht. - -Es wird in OAUTh ein Token Refresh durchgeführt. -Da alle Token nur 2 Stunden gültig sind und der Refresh mit gültigem refresh_token erfolgen muss, -sollten die Intervalle in der Modulkonfiguration weniger als 2 Stunden betragen. -90 Minuten im Standby und 10 Minuten während des Ladens haben sich im Test bewährt. - -Wenn die Intervalle auf mehr als 2 Stunden konfiguriert sind, wird bei der nächsten Abfrage ein Login durchgeführt. -Das benötigt deutlich mehr Zeit und es können die Token anderer Sitzungen, z.B. der smart Control App, -ungültig werden und einen neuen Login benötigen mit Eingabe von User und Passwort. - diff --git a/modules/soc_smarteq/_secrets.py b/modules/soc_smarteq/_secrets.py deleted file mode 100755 index 130434229..000000000 --- a/modules/soc_smarteq/_secrets.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Generate cryptographically strong pseudo-random numbers suitable for -managing secrets such as account authentication, tokens, and similar. - -See PEP 506 for more information. -https://www.python.org/dev/peps/pep-0506/ - -""" - -__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom', - 'token_bytes', 'token_hex', 'token_urlsafe', - 'compare_digest', - ] - - -import base64 -import binascii -import os - -from hmac import compare_digest -from random import SystemRandom - -_sysrand = SystemRandom() - -randbits = _sysrand.getrandbits -choice = _sysrand.choice - -def randbelow(exclusive_upper_bound): - """Return a random int in the range [0, n).""" - if exclusive_upper_bound <= 0: - raise ValueError("Upper bound must be positive.") - return _sysrand._randbelow(exclusive_upper_bound) - -DEFAULT_ENTROPY = 32 # number of bytes to return by default - -def token_bytes(nbytes=None): - """Return a random byte string containing *nbytes* bytes. - - If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_bytes(16) #doctest:+SKIP - b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b' - - """ - if nbytes is None: - nbytes = DEFAULT_ENTROPY - return os.urandom(nbytes) - -def token_hex(nbytes=None): - """Return a random text string, in hexadecimal. - - The string has *nbytes* random bytes, each byte converted to two - hex digits. If *nbytes* is ``None`` or not supplied, a reasonable - default is used. - - >>> token_hex(16) #doctest:+SKIP - 'f9bf78b9a18ce6d46a0cd2b0b86df9da' - - """ - return binascii.hexlify(token_bytes(nbytes)).decode('ascii') - -def token_urlsafe(nbytes=None): - """Return a random URL-safe text string, in Base64 encoding. - - The string has *nbytes* random bytes. If *nbytes* is ``None`` - or not supplied, a reasonable default is used. - - >>> token_urlsafe(16) #doctest:+SKIP - 'Drmhze6EPcv0fN_81Bj-nA' - - """ - tok = token_bytes(nbytes) - return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii') diff --git a/modules/soc_smarteq/soc_smarteq.py b/modules/soc_smarteq/soc_smarteq.py deleted file mode 100755 index 6b4e642e1..000000000 --- a/modules/soc_smarteq/soc_smarteq.py +++ /dev/null @@ -1,460 +0,0 @@ -#!/usr/bin/python3 - -from argparse import ArgumentParser -import requests -import json -import bs4 -import os -import time -import datetime -import pkce -import logging -import pickle -import copy - -# Constants -BASE_URL = "https://id.mercedes-benz.com" -OAUTH_URL = BASE_URL + "/as/authorization.oauth2" -LOGIN_URL = BASE_URL + "/ciam/auth/login" -TOKEN_URL = BASE_URL + "/as/token.oauth2" -#STATUS_URL = "https://oneapp.microservice.smart.com" -STATUS_URL = "https://oneapp.microservice.smart.mercedes-benz.com" -REDIRECT_URI = STATUS_URL -SCOPE = "openid+profile+email+phone+ciam-uid+offline_access" -CLIENT_ID = "70d89501-938c-4bec-82d0-6abb550b0825" -GUID = "280C6B55-F179-4428-88B6-E0CCF5C22A7C" -ACCEPT_LANGUAGE = "de-de" - -TOKENS_REFRESH_THRESHOLD = 3600 -SSL_VERIFY_AUTH = True -SSL_VERIFY_STATUS = True - - - -# helper functions -def nested_key_exists(element: dict, *keys: str) -> bool: - # Check if *keys (nested) exists in `element` (dict). - if not isinstance(element, dict): - raise AttributeError('nested_key_exists() expects dict as first argument - got type ' + str(type(element))) - if len(keys) == 0: - raise AttributeError('nested_key_exists() expects at least two arguments, one given.') - - _element = element - for key in keys: - try: - _element = _element[key] - except KeyError: - return False - return True - - -class smarteq: - def __init__(self, storeFile: str): - self.storeFile = storeFile - self.log = logging.getLogger("soc_smarteq") - debug = os.environ.get('debug', '0') - LOGLEVEL = 'WARN' - if debug == '1': - LOGLEVEL = 'INFO' - if debug == '2': - LOGLEVEL = 'DEBUG' - RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined") - logFile = RAMDISKDIR+'/soc.log' - format = '%(asctime)s %(levelname)s:%(name)s:%(message)s' - datefmt = '%Y-%m-%d %H:%M:%S' - logging.basicConfig(filename=logFile, - filemode='a', - format=format, - datefmt=datefmt, - level=LOGLEVEL) - - # self.method keeps a high level trach of actions - self.method = '' - self.soc_ts = 'n/a' - # self.store is read from ramdisk at start and saved at end. - # currently is contains: - # Tokens: refresh- and access-tokens of OAUTH - # refresh_timestamp: epoch of last refresh_tokens. - - self.session = requests.session() - - self.load_store() - self.oldTokens = copy.deepcopy(self.store['Tokens']) - self.init = True - - def load_store(self): - try: - tf = open(self.storeFile, "rb") - self.store = pickle.load(tf) - if 'Tokens' not in self.store: - self.store['Tokens'] = {} - self.store['refresh_timestamp'] = int(0) - tf.close() - except FileNotFoundError: - self.log.warning("init: no store file found, full reconnect required") - self.store = {} - self.store['Tokens'] = {} - self.store['refresh_timestamp'] = int(0) - except Exception as e: - self.log.debug("init: loading stored data failed, file: " + self.storeFile) - self.store = {} - self.store['Tokens'] = {} - self.store['refresh_timestamp'] = int(0) - - def write_store(self): - try: - tf = open(self.storeFile, "wb") - except Exception as e: - self.log.debug("write_store: Exception " + str(e)) - os.system("sudo rm -f " + self.storeFile) - tf = open(self.storeFile, "wb") - pickle.dump(self.store, tf) - tf.close() - try: - os.chmod(self.storeFile, 0o777) - except Exception as e: - os.system("sudo chmod 0777 " + self.storeFile) - - # set username and password - def set_credentials(self, username: str, password: str): - self.username = username - self.password = password - - # set vin - def set_vin(self, vin: str): - self.vin = vin - - # set chargepoint number - def set_chargepoint(self, chargepoint: str): - self.chargepoint = chargepoint - - # ===== get resume string ====== - def get_resume(self) -> str: - response_type = "code" - self.code_verifier, self.code_challenge = pkce.generate_pkce_pair() - self.code_challenge_method = "S256" - url = OAUTH_URL + '?client_id=' + CLIENT_ID + '&response_type=' + response_type + '&scope=' + SCOPE - url = url + '&redirect_uri=' + REDIRECT_URI - url = url + '&code_challenge=' + self.code_challenge + '&code_challenge_method=' + self.code_challenge_method - headers = { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language": ACCEPT_LANGUAGE, - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_5_1 like Mac OS X)\ - AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" - } - - try: - response = self.session.get(url, headers=headers, verify=SSL_VERIFY_AUTH) - self.log.debug("get_resume: status_code = " + str(response.status_code)) - self.log.debug("get_resume: text = " + str(response.text)) - soup = bs4.BeautifulSoup(response.text, 'html.parser') - - for cd in soup.findAll(text=True): - if "CDATA" in cd: - self.log.debug("get_resume: cd.CData= " + str(cd)) - for w in cd.split(','): - if w.find("const initialState = ") != -1: - iS = w - if iS: - js = iS.split('{')[1].split('}')[0].replace('\\', '').replace('\\"', '"').replace('""', '"') - self.resume = js[1:len(js)-1].split(':')[1][2:] - self.log.debug("get_resume: resume = " + self.resume) - except Exception as e: - self.log.error('get_resume: Exception: ' + str(e)) - return self.resume - - # login to website, return (intermediate) token - def login(self) -> str: - self.resume = self.get_resume() - url = LOGIN_URL + "/pass" - headers = { - "Content-Type": "application/json", - "Accept": "application/json, text/plain, */*", - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_5_1 like Mac OS X)\ - AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", - "Referer": LOGIN_URL, - "Accept-Language": ACCEPT_LANGUAGE - } - d = {} - d['username'] = self.username - d['password'] = self.password - d['rememberMe'] = 'true' - data = json.dumps(d) - data = json.dumps({'username': self.username, - 'password': self.password, - 'rememberMe': 'true'}) - - try: - response = self.session.post(url, headers=headers, data=data, verify=SSL_VERIFY_AUTH) - self.log.debug("login: status_code = " + str(response.status_code)) - if response.status_code > 400: - self.log.error("login: failed, status_code = " + str(response.status_code) + - ", check username/password") - token = "" - else: - result_json = json.loads(str(bs4.BeautifulSoup(response.text, 'html.parser'))) - self.log.debug("login: result_json:\n" + json.dumps(result_json)) - token = result_json['token'] - self.log.debug("login: token = " + token) - except Exception as e: - self.log.error('login: Exception: ' + str(e)) - token = "" - return token - - # get code - def get_code(self) -> str: - token = self.login() - if token == "": - self.log.error("login: Login failed - check username/password") - return "" - url = BASE_URL + '/' + self.resume - headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json, text/plain, */*", - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_5_1 like Mac OS X)\ - AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", - "Referer": LOGIN_URL, - "Accept-Language": ACCEPT_LANGUAGE, - } - # d = {} - # d['token'] = token - # data = json.dumps(d) - data = json.dumps({'token': token}) - - try: - response = self.session.post(url, headers=headers, data=data, verify=SSL_VERIFY_AUTH) - code = response.url.split('?')[1].split('=')[1] - self.log.debug("get_code: code=" + code) - except Exception as e: - self.log.error("get_code: Exception: " + str(e)) - return code - - # get Tokens - def get_tokens(self) -> dict: - self.method += " 3-full (re)connect" - code = self.get_code() - if code == "": - self.log.warn("get_tokens: get_code failed") - return {} - url = TOKEN_URL - headers = { - "Accept": "*/*", - "User-Agent": "sOAF/202108260942 CFNetwork/978.0.7 Darwin/18.7.0", - "Accept-Language": ACCEPT_LANGUAGE, - "Content-Type": "application/x-www-form-urlencoded", - } - data = "grant_type=authorization_code&code=" + code + "&code_verifier=" + self.code_verifier +\ - "&redirect_uri=" + REDIRECT_URI + "&client_id=" + CLIENT_ID - - try: - response = self.session.post(url, headers=headers, data=data, verify=SSL_VERIFY_AUTH) - self.log.debug("get_tokens: status_code = " + str(response.status_code)) - Tokens = json.loads(response.text) - if not Tokens['access_token']: - self.log.warn("get_tokens: no access_token found") - Tokens = {} - else: - self.log.debug("Tokens=\n" + json.dumps(Tokens, indent=4)) - except Exception as e: - self.log.exception("get_tokens: Exception: " + str(e)) - return Tokens - - # refresh tokens - def refresh_tokens(self) -> dict: - self.method += " 2-refresh_tokens" - url = TOKEN_URL - headers = { - "Accept": "*/*", - "User-Agent": "sOAF/202108260942 CFNetwork/978.0.7 Darwin/18.7.0", - "Accept-Language": ACCEPT_LANGUAGE, - "Content-Type": "application/x-www-form-urlencoded", - } - # data = {'grant_type': 'refresh_token', - # 'client_id': CLIENT_ID, - # 'refresh_token': self.store['Tokens']['refresh_token']} - data = "grant_type=refresh_token&client_id=" + CLIENT_ID + "&refresh_token=" +\ - self.store['Tokens']['refresh_token'] - - try: - response = self.session.post(url, - headers=headers, - data=data, - verify=SSL_VERIFY_AUTH, - allow_redirects=False, - timeout=(30, 30)) - self.log.debug("refresh_tokens: status_code = " + str(response.status_code)) - self.log.debug("refresh_tokens: text = " + str(response.text)) - newTokens = json.loads(response.text) - if 'error' in newTokens and newTokens['error'] == 'unauthorized': - self.log.warning("refresh_tokens: error: " + newTokens['error'] + ', ' + newTokens['error_description']) - if 'access_token' not in newTokens: - self.log.debug("refresh_tokens: new access_token not found") - newTokens['access_token'] = "" - if 'refresh_token' not in newTokens: - self.log.debug("refresh_tokens: new refresh_token not found") - newTokens['refresh_token'] = "" - self.log.debug("refresh_tokens: newTokens=\n" + json.dumps(newTokens, indent=4)) - except Exception as e: - self.log.error("refresh_tokens: Exception: " + str(e)) - newTokens['access_token'] = "" - newTokens['refresh_token'] = "" - return newTokens - - # reconnect to Server - def reconnect(self) -> dict: - # check if we have a refresh token and last refresh was more then 1h ago (3600s) - if 'refresh_token' in self.store['Tokens']: - now = int(time.time()) - secs_since_refresh = now - self.store['refresh_timestamp'] - if secs_since_refresh > TOKENS_REFRESH_THRESHOLD: - # try to refresh tokens - new_tokens = self.refresh_tokens() - self.store['refresh_timestamp'] = int(time.time()) - _ref = True - else: - # keep existing tokens - return self.store['Tokens'] - else: - self.log.debug("reconnect: refresh_token not found in self.store['Tokens']=" + - json.dumps(self.store['Tokens'], indent=4)) - new_tokens = {'refresh_token': '', 'access_token': ''} - _ref = False - self.log.debug("reconnect: new_tokens=" + json.dumps(new_tokens, indent=4)) - if new_tokens['access_token'] == '': - if _ref: - self.log.warning("reconnect: refresh access_token failed, try full reconnect") - Tokens = self.get_tokens() - else: - self.log.debug("reconnect: refresh access_token successful") - Tokens = self.store['Tokens'] # replace expired access and refresh token by new tokens - for key in new_tokens: - Tokens[key] = new_tokens[key] - self.log.debug("reconnect: replace Tokens[" + key + "], new value: " + str(Tokens[key])) - - return Tokens - - # get Soc of Vehicle - def get_status(self, vin: str) -> int: - self.method += " 1-get_status" - if self.init: - url = STATUS_URL + "/seqc/v0/vehicles/" + vin +\ - "/init-data?requestedData=BOTH&countryCode=DE&locale=de-DE" - else: - url = STATUS_URL + "/seqc/v0/vehicles/" + vin + "/refresh-data" - self.init = False - - headers = { - "accept": "*/*", - "accept-language": "de-DE;q=1.0", - "authorization": "Bearer " + self.store['Tokens']['access_token'], - "x-applicationname": CLIENT_ID, - "user-agent": "Device: iPhone 6; OS-version: iOS_12.5.1; App-Name: smart EQ control; App-Version: 3.0;\ - Build: 202108260942; Language: de_DE", - "guid": GUID - } - - try: - response = self.session.get(url, headers=headers, verify=SSL_VERIFY_STATUS) - res = json.loads(response.text) - res_json = json.dumps(res, indent=4) - if nested_key_exists(res, 'precond', 'data', 'soc', 'value'): - res_json = json.dumps(res['precond']['data']['soc'], indent=4) - try: - soc = res['precond']['data']['soc']['value'] - _ts = res['precond']['data']['soc']['ts'] - self.soc_ts = datetime.datetime.fromtimestamp(_ts).strftime('%Y-%m-%d %H:%M:%S') - self.log.debug("get_status: result json:\n" + res_json) - except: - soc = -1 - elif 'error' in res and res['error'] == 'unauthorized': - self.log.warning("get_status: access_token expired - try refresh") - self.log.debug("get_status: error - result json:\n" + res_json) - soc = -1 - - except Exception as e: - self.log.error("get_status: Exception: " + str(e)) - self.log.error("get_status: result:\n" + res_json) - soc = -1 - if "Vehicle not found" in res_json: - soc = -2 - return soc - - # fetch soc in 3 stages: - # 1. get_status via stored access_token - # 2. if expired: refresh_access_token using id and refresh token, then get_status - # 3. if refresh token expired: login, get tokens, then get_status - def fetch_soc(self) -> int: - soc = -1 - try: - if 'refresh_token' in self.store['Tokens']: - self.store['Tokens'] = self.reconnect() - if 'access_token' in self.store['Tokens']: - soc = self.get_status(self.vin) - if soc > 0: - self.log.debug("fetch_soc: 1st attempt successful") - else: - self.log.debug("fetch_soc: 1st attempt failed - soc=" + str(soc)) - - if soc == -1: - self.log.debug("fetch_soc: (re)connecting ...") - self.store['Tokens'] = self.reconnect() - if 'access_token' in self.store['Tokens']: - soc = self.get_status(self.vin) - if soc > 0: - self.log.debug("fetch_soc: 2nd attempt successful") - else: - self.log.warning("fetch_soc: 2nd attempt failed - soc=" + str(soc)) - else: - self.log.error("fetch_soc: (re-)connect failed") - soc = 0 - elif soc == -2: - self.log.error("fetch_soc: failed, Vehicle not found, check VIN") - soc = 0 - - except Exception as e: - self.log.error("fetch_soc: exception, (re-)connecting ..." + str(e)) - self.store['Tokens'] = self.reconnect() - if 'access_token' in self.store['Tokens']: - soc = self.get_status(self.vin) - self.log.info("Lp" + self.chargepoint + - " SOC: " + str(soc) + '%' + - '@' + self.soc_ts + - ', Method: ' + self.method) - - if self.store['Tokens'] != self.oldTokens: - self.log.debug("reconnect: tokens changed, store token file") - self.write_store() - - return soc - - -# main program -def main(): - parser = ArgumentParser() - parser.add_argument("-v", "--vin", - help="VIN of vehicle", metavar="VIN", required=True) - parser.add_argument("-u", "--user", - help="user", metavar="user", required=True) - parser.add_argument("-p", "--password", - help="password", metavar="password", required=True) - parser.add_argument("-c", "--chargepoint", - help="chargepoint", metavar="chargepoint", required=True) - args = vars(parser.parse_args()) - user_id = args['user'] - password = args['password'] - vin = args['vin'] - chargepoint = args['chargepoint'] - - RAMDISKDIR = os.environ.get("RAMDISKDIR", "undefined") - storeFile = RAMDISKDIR+'/soc_smarteq_store_lp'+chargepoint - - Smart = smarteq(storeFile) - Smart.set_credentials(user_id, password) - Smart.set_vin(vin) - Smart.set_chargepoint(chargepoint) - soc = Smart.fetch_soc() - print(soc) - - -if __name__ == "__main__": - main() diff --git a/modules/soc_tesla/tesla.py b/modules/soc_tesla/tesla.py index 2911f7b23..2a2d16474 100755 --- a/modules/soc_tesla/tesla.py +++ b/modules/soc_tesla/tesla.py @@ -318,7 +318,7 @@ def refreshToken(email): def listCars(): myList = [] - myVehicles = requestData('vehicles') + myVehicles = requestData('products') for index, car in enumerate(json.loads(myVehicles)["response"]): myList.append(json.loads("{\"id\":\"%s\", \"vin\":\"%s\", \"name\":\"%s\"}" % (index, car["vin"], car["display_name"]))) @@ -326,7 +326,7 @@ def listCars(): def getVehicleIdByVin(vin): - myVehicles = requestData('vehicles') + myVehicles = requestData('products') for car in json.loads(myVehicles)["response"]: if(verbose): eprint("VIN: %s" % (car["vin"])) @@ -341,7 +341,7 @@ def getVehicleIdByVin(vin): def getVehicleIdByIndex(index): - myVehicles = requestData('vehicles') + myVehicles = requestData('products') myVehicleId = json.loads(myVehicles)["response"][index]["id"] if(verbose): eprint("vehicle_id for entry %d: %s" % (index, str(myVehicleId))) diff --git a/modules/soc_vwid/main.sh b/modules/soc_vwid/main.sh index ea94b811d..8269dddc6 100755 --- a/modules/soc_vwid/main.sh +++ b/modules/soc_vwid/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 section 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 + OPENWBBASEDIR=$(cd `dirname $0`/../../ && pwd) RAMDISKDIR="$OPENWBBASEDIR/ramdisk" MODULEDIR=$(cd `dirname $0` && pwd) diff --git a/modules/speicher_sungrow/main.sh b/modules/speicher_sungrow/main.sh index a9a634c35..e73f14ff3 100755 --- a/modules/speicher_sungrow/main.sh +++ b/modules/speicher_sungrow/main.sh @@ -10,7 +10,7 @@ else MYLOGFILE="$RAMDISKDIR/bat.log" fi -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "bat" "$speicher1_ip" "$sungrowspeicherid" >>"$MYLOGFILE" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "bat" "$speicher1_ip" "$sungrowspeicherport" "$sungrowspeicherid" >>"$MYLOGFILE" 2>&1 ret=$? openwbDebugLog $DMOD 2 "BAT RET: $ret" diff --git a/modules/wr2_kostalpikovar2/main.sh b/modules/wr2_kostalpikovar2/main.sh index efec1d9f5..dc22182f7 100755 --- a/modules/wr2_kostalpikovar2/main.sh +++ b/modules/wr2_kostalpikovar2/main.sh @@ -21,7 +21,7 @@ openwbDebugLog ${DMOD} 2 "WR User: ${wr2_piko2_user}" openwbDebugLog ${DMOD} 2 "WR Passwort: ${wr2_piko2_pass}" openwbDebugLog ${DMOD} 2 "WR URL: ${wr2_piko2_url}" -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "wr_kostalpikovar2.kostal_piko_var2" 2 "${wr2_piko2_url}" "${wr2_piko2_user}" "${wr2_piko2_pass}" >>"$MYLOGFILE" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.kostal_piko_old.device" "inverter" "${wr2_piko2_url}" "${wr2_piko2_user}" "${wr2_piko2_pass}" 2 >>"$MYLOGFILE" 2>&1 ret=$? openwbDebugLog ${DMOD} 2 "RET: ${ret}" diff --git a/modules/wr_kostalpikovar2/kostal_piko_var2.py b/modules/wr_kostalpikovar2/kostal_piko_var2.py deleted file mode 100755 index e1711152b..000000000 --- a/modules/wr_kostalpikovar2/kostal_piko_var2.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -import logging -import re -from typing import List - -import requests - -from helpermodules.cli import run_using_positional_cli_args -from modules.common.component_state import InverterState -from modules.common.store import get_inverter_value_store - -log = logging.getLogger("Kostal Piko Var2") - - -def parse_kostal_piko_var2_html(html: str): - # power may be a string "xxx" when the inverter is offline, so we cannot match as a number - # state is just for debugging currently known states: - # - Aus - # - Leerlauf - result = re.search( - r"aktuell\s*]*>\s*([^<]+).*" - r"Gesamtenergie\s*]*>\s*(\d+).*" - r"Status\s*]*>\s*([^<]+)", - html, - re.DOTALL - ) - if result is None: - raise Exception("Given HTML does not match the expected regular expression. Ignoring.") - log.debug("Inverter data: state=%s, power=%s, exported=%s" % (result.group(3), result.group(1), result.group(2))) - try: - power = -int(result.group(1)) - except ValueError: - log.info("Inverter power is not a number! Inverter may be offline. Setting power to 0 W.") - power = 0 - return InverterState( - exported=int(result.group(2)) * 1000, - power=power - ) - - -def update(num: int, wr_piko2_url: str, wr_piko2_user: str, wr_piko2_pass: str): - log.debug("Beginning update") - response = requests.get(wr_piko2_url, verify=False, auth=(wr_piko2_user, wr_piko2_pass), timeout=10) - response.raise_for_status() - get_inverter_value_store(num).set(parse_kostal_piko_var2_html(response.text)) - log.debug("Update completed successfully") - - -def main(argv: List[str]): - run_using_positional_cli_args(update, argv) diff --git a/modules/wr_kostalpikovar2/kostal_piko_var2_test.py b/modules/wr_kostalpikovar2/kostal_piko_var2_test.py deleted file mode 100755 index 0f570950c..000000000 --- a/modules/wr_kostalpikovar2/kostal_piko_var2_test.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path - -import pytest - -from kostal_piko_var2 import parse_kostal_piko_var2_html -from test_utils.mock_ramdisk import MockRamdisk - - -@pytest.fixture -def mock_ramdisk(monkeypatch): - return MockRamdisk(monkeypatch) - - -def test_parse_html(mock_ramdisk: MockRamdisk): - # setup - sample_html = (Path(__file__).parent / "kostal_piko_var2_test_sample.html").read_text() - - # execution - actual = parse_kostal_piko_var2_html(sample_html) - - # evaluation - assert actual.power == -50 - assert actual.exported == 73288000 - - -def test_parse_html_off(mock_ramdisk: MockRamdisk): - # setup - sample_html = (Path(__file__).parent / "kostal_piko_var2_test_sample_off.html").read_text() - - # execution - actual = parse_kostal_piko_var2_html(sample_html) - - # evaluation - assert actual.power == 0 - assert actual.exported == 42906000 diff --git a/modules/wr_kostalpikovar2/main.sh b/modules/wr_kostalpikovar2/main.sh index 962eafe91..04c2d9228 100755 --- a/modules/wr_kostalpikovar2/main.sh +++ b/modules/wr_kostalpikovar2/main.sh @@ -21,7 +21,7 @@ openwbDebugLog ${DMOD} 2 "WR User: ${wr_piko2_user}" openwbDebugLog ${DMOD} 2 "WR Passwort: ${wr_piko2_pass}" openwbDebugLog ${DMOD} 2 "WR URL: ${wr_piko2_url}" -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "wr_kostalpikovar2.kostal_piko_var2" 1 "${wr_piko2_url}" "${wr_piko2_user}" "${wr_piko2_pass}" >>"$MYLOGFILE" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.kostal_piko_old.device" "inverter" "${wr_piko2_url}" "${wr_piko2_user}" "${wr_piko2_pass}" 1 >>"$MYLOGFILE" 2>&1 ret=$? openwbDebugLog ${DMOD} 2 "RET: ${ret}" diff --git a/modules/wr_sungrow/main.sh b/modules/wr_sungrow/main.sh index 17df513c1..597f84fff 100755 --- a/modules/wr_sungrow/main.sh +++ b/modules/wr_sungrow/main.sh @@ -15,6 +15,6 @@ if [[ "$wattbezugmodul" == "bezug_sungrow" ]]; then else read_counter=0 fi -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "inverter" "$speicher1_ip" "$sungrowspeicherid" "1" "$read_counter" "$sungrowsr" >>"$MYLOGFILE" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.devices.sungrow.device" "inverter" "$speicher1_ip" "$sungrowspeicherport" "$sungrowspeicherid" "1" "$read_counter" "$sungrowsr" >>"$MYLOGFILE" 2>&1 cat "$RAMDISKDIR/pvwatt" diff --git a/openwb-install.sh b/openwb-install.sh index ee01bb55f..5c79119b3 100755 --- a/openwb-install.sh +++ b/openwb-install.sh @@ -1,6 +1,13 @@ #!/bin/bash echo "install required packages..." +# check for outdated sources.list (Stretch only) +if grep -q -e "^deb http://raspbian.raspberrypi.org/raspbian/ stretch" /etc/apt/sources.list; then + echo "sources.list outdated! upgrading..." + sudo sed -i "s/^deb http:\/\/raspbian.raspberrypi.org\/raspbian\/ stretch/deb http:\/\/legacy.raspbian.org\/raspbian\/ stretch/g" /etc/apt/sources.list +else + echo "sources.list already updated" +fi apt-get update apt-get -q -y install vim bc apache2 php php-gd php-curl php-xml php-json libapache2-mod-php jq raspberrypi-kernel-headers i2c-tools git mosquitto mosquitto-clients socat python-pip python3-pip sshpass echo "...done" @@ -94,7 +101,7 @@ echo "check for paho-mqtt" if python3 -c "import paho.mqtt.publish as publish" &> /dev/null; then echo 'mqtt installed...' else - sudo pip3 install paho-mqtt + sudo pip3 install "paho-mqtt<2.0.0" fi #Adafruit install diff --git a/packages/modules/common/abstract_counter.py b/packages/modules/common/abstract_counter.py new file mode 100644 index 000000000..b6b01ce24 --- /dev/null +++ b/packages/modules/common/abstract_counter.py @@ -0,0 +1,38 @@ +from abc import abstractmethod +from typing import List, Tuple + +from modules.common import modbus + + +class AbstractCounter: + @abstractmethod + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + pass + + @abstractmethod + def get_currents(self) -> List[float]: + return [0]*3 + + @abstractmethod + def get_exported(self) -> float: + return 0 + + @abstractmethod + def get_frequency(self) -> float: + return 50 + + @abstractmethod + def get_imported(self) -> float: + return 0 + + @abstractmethod + def get_power(self) -> Tuple[List[float], float]: + return [0]*3, 0 + + @abstractmethod + def get_power_factors(self) -> List[float]: + return [0]*3 + + @abstractmethod + def get_voltages(self) -> List[float]: + return [230]*3 diff --git a/packages/modules/common/b23.py b/packages/modules/common/b23.py index f162a7dd1..8368dd146 100644 --- a/packages/modules/common/b23.py +++ b/packages/modules/common/b23.py @@ -2,27 +2,34 @@ from typing import List, Tuple from modules.common import modbus +from modules.common.abstract_counter import AbstractCounter from modules.common.modbus import ModbusDataType -class B23: +class B23(AbstractCounter): def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.client = client self.id = modbus_id - def get_imported(self) -> float: - return self.client.read_holding_registers(0x5000, ModbusDataType.UINT_64, unit=self.id) * 10 - - def get_frequency(self) -> float: - return self.client.read_holding_registers(0x5B2C, ModbusDataType.INT_16, unit=self.id) / 100 - def get_currents(self) -> List[float]: return [val / 100 for val in self.client.read_holding_registers( 0x5B0C, [ModbusDataType.UINT_32]*3, unit=self.id)] + def get_frequency(self) -> float: + return self.client.read_holding_registers(0x5B2C, ModbusDataType.UINT_16, unit=self.id) / 100 + + def get_imported(self) -> float: + return self.client.read_holding_registers(0x5000, ModbusDataType.UINT_64, unit=self.id) * 10 + def get_power(self) -> Tuple[List[float], float]: - power = self.client.read_holding_registers(0x5B14, ModbusDataType.INT_32, unit=self.id) / 100 - return [0]*3, power + # reading of total power and power per phase in one call + powers = [val / 100 for val in self.client.read_holding_registers( + 0x5B14, [ModbusDataType.INT_32]*4, unit=self.id)] + return powers[1:4], powers[0] + + def get_power_factors(self) -> List[float]: + return [val / 1000 for val in self.client.read_holding_registers( + 0x5B3B, [ModbusDataType.INT_16]*3, unit=self.id)] def get_voltages(self) -> List[float]: return [val / 10 for val in self.client.read_holding_registers( diff --git a/packages/modules/common/component_state.py b/packages/modules/common/component_state.py index 215ec9f26..eb08b92ec 100644 --- a/packages/modules/common/component_state.py +++ b/packages/modules/common/component_state.py @@ -1,8 +1,25 @@ -from typing import List, Optional +from typing import List, Optional, Tuple from helpermodules.auto_str import auto_str +def _calculate_powers_and_currents(currents: Optional[List[float]], + powers: Optional[List[float]], + voltages: Optional[List[float]]) -> Tuple[List[float]]: + if voltages is None: + voltages = [230.0]*3 + if powers is None: + if currents is None: + powers = [0.0]*3 + else: + powers = [currents[i]*voltages[i] for i in range(0, 3)] + if currents is None and powers: + currents = [powers[i]/voltages[i] for i in range(0, 3)] + if currents and powers: + currents = [currents[i]*-1 if powers[i] < 0 and currents[i] > 0 else currents[i] for i in range(0, 3)] + return currents, powers, voltages + + @auto_str class BatState: def __init__( @@ -47,20 +64,7 @@ def __init__( power_factors: actual power factors for 3 phases frequency: actual grid frequency in Hz """ - if voltages is None: - voltages = [230.0]*3 - self.voltages = voltages - if powers is None: - if currents is None: - powers = [0.0]*3 - else: - powers = [currents[i]*voltages[i] for i in range(0, 3)] - self.powers = powers - if currents is None and powers: - currents = [powers[i]/voltages[i] for i in range(0, 3)] - if currents and powers: - currents = [currents[i]*-1 if powers[i] < 0 and currents[i] > 0 else currents[i] for i in range(0, 3)] - self.currents = currents + self.currents, self.powers, self.voltages = _calculate_powers_and_currents(currents, powers, voltages) if power_factors is None: power_factors = [0.0]*3 self.power_factors = power_factors @@ -115,21 +119,16 @@ def __init__(self, imported: float = 0, exported: float = 0, power: float = 0, + powers: Optional[List[float]] = None, voltages: Optional[List[float]] = None, currents: Optional[List[float]] = None, power_factors: Optional[List[float]] = None, charge_state: bool = False, plug_state: bool = False, - rfid: Optional[str] = None): - if voltages is None: - voltages = [0.0]*3 - self.voltages = voltages - if currents is None: - currents = [0.0]*3 - self.currents = currents - if power_factors is None: - power_factors = [0.0]*3 - self.power_factors = power_factors + rfid: Optional[str] = None, + frequency: float = 50): + self.currents, self.powers, self.voltages = _calculate_powers_and_currents(currents, powers, voltages) + self.frequency = frequency self.imported = imported self.exported = exported self.power = power @@ -137,3 +136,6 @@ def __init__(self, self.charge_state = charge_state self.plug_state = plug_state self.rfid = rfid + if power_factors is None: + power_factors = [0.0]*3 + self.power_factors = power_factors diff --git a/packages/modules/common/evse.py b/packages/modules/common/evse.py index 6fd98557c..2b20af229 100644 --- a/packages/modules/common/evse.py +++ b/packages/modules/common/evse.py @@ -35,14 +35,14 @@ def __init__(self, modbus_id: int, client: modbus.ModbusSerialClient_) -> None: def get_plug_charge_state(self) -> Tuple[bool, bool, float]: set_current, _, state_number = self.client.read_holding_registers( 1000, [ModbusDataType.UINT_16]*3, unit=self.id) - # remove leading zeors + # remove leading zeros set_current = int(set_current) log.debug("Gesetzte Stromstärke EVSE: "+str(set_current) + ", Status: "+str(state_number)+", Modbus-ID: "+str(self.id)) state = EvseState(state_number) if state == EvseState.FAILURE: raise FaultState.error("Unbekannter Zustand der EVSE: State " + - str(state)+", Sollstromstärke: "+str(set_current)) + str(state)+", Soll-Stromstärke: "+str(set_current)) plugged = state.plugged charging = set_current > 0 if state.charge_enabled else False return plugged, charging, set_current diff --git a/packages/modules/common/lovato.py b/packages/modules/common/lovato.py index 153b4dd90..504f37687 100644 --- a/packages/modules/common/lovato.py +++ b/packages/modules/common/lovato.py @@ -2,10 +2,11 @@ from modules.common import modbus from typing import List, Tuple +from modules.common.abstract_counter import AbstractCounter from modules.common.modbus import ModbusDataType -class Lovato: +class Lovato(AbstractCounter): def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.client = client self.id = modbus_id diff --git a/packages/modules/common/mpm3pm.py b/packages/modules/common/mpm3pm.py index 56ea2b907..13ff71b77 100644 --- a/packages/modules/common/mpm3pm.py +++ b/packages/modules/common/mpm3pm.py @@ -2,10 +2,11 @@ from typing import List, Tuple from modules.common import modbus +from modules.common.abstract_counter import AbstractCounter from modules.common.modbus import ModbusDataType -class Mpm3pm: +class Mpm3pm(AbstractCounter): def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.client = client self.id = modbus_id diff --git a/packages/modules/common/sdm.py b/packages/modules/common/sdm.py index 4512dc38c..bdfef8f8e 100644 --- a/packages/modules/common/sdm.py +++ b/packages/modules/common/sdm.py @@ -2,10 +2,11 @@ from typing import List, Tuple from modules.common import modbus +from modules.common.abstract_counter import AbstractCounter from modules.common.modbus import ModbusDataType -class Sdm: +class Sdm(AbstractCounter): def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.client = client self.id = modbus_id diff --git a/packages/modules/common/store/_chargepoint.py b/packages/modules/common/store/_chargepoint.py index 00a79dbff..422f5be03 100644 --- a/packages/modules/common/store/_chargepoint.py +++ b/packages/modules/common/store/_chargepoint.py @@ -31,6 +31,8 @@ def set(self, state: ChargepointState) -> None: pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/imported", state.imported, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/exported", state.exported, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/power", state.power, 2) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/powers", state.powers, 2) + pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/frequency", state.frequency, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/phases_in_use", state.phases_in_use, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/charge_state", state.charge_state, 2) pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/plug_state", state.plug_state, 2) diff --git a/packages/modules/devices/batterx/batterx_test.py b/packages/modules/devices/batterx/batterx_test.py index 026c258b7..e148bb22c 100644 --- a/packages/modules/devices/batterx/batterx_test.py +++ b/packages/modules/devices/batterx/batterx_test.py @@ -4,9 +4,10 @@ from modules.common.component_state import BatState, CounterState, InverterState -from modules.devices.batterx import bat, counter, device, inverter +from modules.devices.batterx import bat, counter, inverter from modules.devices.batterx.config import (BatterX, BatterXBatSetup, BatterXConfiguration, BatterXCounterSetup, BatterXInverterSetup) +from modules.devices.batterx.device import create_device def test_batterx(monkeypatch, requests_mock: requests_mock.mock): @@ -19,7 +20,7 @@ def test_batterx(monkeypatch, requests_mock: requests_mock.mock): monkeypatch.setattr(inverter, 'get_inverter_value_store', Mock(return_value=mock_inverter_value_store)) requests_mock.get("http://1.1.1.1/api.php?get=currentstate", json=SAMPLE) - dev = device.Device(BatterX(configuration=BatterXConfiguration(ip_address="1.1.1.1"))) + dev = create_device(BatterX(configuration=BatterXConfiguration(ip_address="1.1.1.1"))) dev.add_component(BatterXBatSetup(id=2)) dev.add_component(BatterXCounterSetup(id=0)) dev.add_component(BatterXInverterSetup(id=1)) diff --git a/packages/modules/devices/batterx/device.py b/packages/modules/devices/batterx/device.py index 001cbb190..2969d884b 100644 --- a/packages/modules/devices/batterx/device.py +++ b/packages/modules/devices/batterx/device.py @@ -1,72 +1,56 @@ #!/usr/bin/env python3 import logging -from typing import Dict, Optional, Union, List +from typing import Iterable, Optional, Union, List -from dataclass_utils import dataclass_from_dict from helpermodules.cli import run_using_positional_cli_args -from modules.common.abstract_device import AbstractDevice, DeviceDescriptor +from modules.common.abstract_device import DeviceDescriptor from modules.common.component_context import MultiComponentUpdateContext +from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater from modules.common.store import get_inverter_value_store from modules.devices.batterx import bat, external_inverter from modules.devices.batterx import counter from modules.devices.batterx import inverter -from modules.devices.batterx.config import BatterX, BatterXBatSetup, BatterXCounterSetup, BatterXInverterSetup +from modules.devices.batterx.config import (BatterX, BatterXBatSetup, BatterXCounterSetup, + BatterXExternalInverterSetup, BatterXInverterSetup) from modules.common import req log = logging.getLogger(__name__) batterx_component_classes = Union[bat.BatterXBat, counter.BatterXCounter, - inverter.BatterXInverter] - - -class Device(AbstractDevice): - COMPONENT_TYPE_TO_CLASS = { - "bat": bat.BatterXBat, - "counter": counter.BatterXCounter, - "inverter": inverter.BatterXInverter, - } - - def __init__(self, device_config: Union[Dict, BatterX]) -> None: - self.components = {} # type: Dict[str, batterx_component_classes] - try: - self.device_config = dataclass_from_dict(BatterX, device_config) - except Exception: - log.exception("Fehler im Modul "+self.device_config.name) - - def add_component(self, component_config: Union[Dict, - BatterXBatSetup, - BatterXCounterSetup, - BatterXInverterSetup]) -> None: - if isinstance(component_config, Dict): - component_type = component_config["type"] - else: - component_type = component_config.type - component_config = dataclass_from_dict(COMPONENT_TYPE_TO_MODULE[ - component_type].component_descriptor.configuration_factory, component_config) - if component_type in self.COMPONENT_TYPE_TO_CLASS: - self.components["component"+str(component_config.id)] = (self.COMPONENT_TYPE_TO_CLASS[component_type]( - self.device_config.id, component_config)) - else: - raise Exception( - "illegal component type " + component_type + ". Allowed values: " + - ','.join(self.COMPONENT_TYPE_TO_CLASS.keys()) - ) - - def update(self) -> None: - log.debug("Start device reading " + str(self.components)) - if self.components: - with MultiComponentUpdateContext(self.components): - resp_json = req.get_http_session().get( - 'http://' + self.device_config.configuration.ip_address + '/api.php?get=currentstate', + inverter.BatterXInverter, external_inverter.BatterXExternalInverter] + + +def create_device(device_config: BatterX): + def create_bat_component(component_config: BatterXBatSetup): + return bat.BatterXBat(device_config.id, component_config) + + def create_counter_component(component_config: BatterXCounterSetup): + return counter.BatterXCounter(device_config.id, component_config) + + def create_inverter_component(component_config: BatterXInverterSetup): + return inverter.BatterXInverter(device_config.id, component_config) + + def create_external_inverter_component(component_config: BatterXExternalInverterSetup): + return external_inverter.BatterXExternalInverter(device_config.id, component_config) + + def update_components(components: Iterable[batterx_component_classes]): + resp_json = req.get_http_session().get( + 'http://' + device_config.configuration.ip_address + '/api.php?get=currentstate', timeout=5).json() - for component in self.components: - self.components[component].update(resp_json) - else: - log.warning( - self.device_config.name + - ": Es konnten keine Werte gelesen werden, da noch keine Komponenten konfiguriert wurden." - ) + for component in components: + component.update(resp_json) + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + external_inverter=create_external_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) COMPONENT_TYPE_TO_MODULE = { @@ -87,7 +71,7 @@ def read_legacy( device_config = BatterX() device_config.configuration.ip_address = ip_address - dev = Device(device_config) + dev = create_device(device_config) dev = _add_component(dev, component_type, num) if evu_counter == "bezug_batterx": dev = _add_component(dev, "counter", 0) @@ -115,7 +99,7 @@ def read_legacy( get_inverter_value_store(num).set(state) -def _add_component(dev: Device, component_type: str, num: Optional[int]) -> Device: +def _add_component(dev: ConfigurableDevice, component_type: str, num: Optional[int]) -> ConfigurableDevice: if component_type in COMPONENT_TYPE_TO_MODULE: component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() else: diff --git a/packages/modules/devices/batterx/external_inverter.py b/packages/modules/devices/batterx/external_inverter.py index 95fa69a56..8cc7c664a 100644 --- a/packages/modules/devices/batterx/external_inverter.py +++ b/packages/modules/devices/batterx/external_inverter.py @@ -19,7 +19,7 @@ def __init__(self, device_id: int, component_config: Union[Dict, BatterXExternal self.component_info = ComponentInfo.from_component_config(self.component_config) def get_power(self, resp: Dict) -> float: - return resp["2913"]["0"] * -1 + return resp["2913"]["3"] * -1 def update(self, resp: Dict) -> None: power = self.get_power(resp) diff --git a/packages/modules/devices/kostal_piko_old/__init__.py b/packages/modules/devices/kostal_piko_old/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/modules/devices/kostal_piko_old/config.py b/packages/modules/devices/kostal_piko_old/config.py new file mode 100644 index 000000000..103dfb7a1 --- /dev/null +++ b/packages/modules/devices/kostal_piko_old/config.py @@ -0,0 +1,40 @@ +from typing import Optional +from helpermodules.auto_str import auto_str +from modules.common.component_setup import ComponentSetup + + +@auto_str +class KostalPikoOldConfiguration: + def __init__(self, ip_address: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None): + self.ip_address = ip_address + self.user = user + self.password = password + + +@auto_str +class KostalPikoOld: + def __init__(self, + name: str = "Kostal Piko (alte Generation)", + type: str = "kostal_piko_old", + id: int = 0, + configuration: KostalPikoOldConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or KostalPikoOldConfiguration() + + +@auto_str +class KostalPikoOldInverterConfiguration: + def __init__(self): + pass + + +@auto_str +class KostalPikoOldInverterSetup(ComponentSetup[KostalPikoOldInverterConfiguration]): + def __init__(self, + name: str = "Kostal Piko (alte Generation) Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: KostalPikoOldInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or KostalPikoOldInverterConfiguration()) diff --git a/packages/modules/devices/kostal_piko_old/device.py b/packages/modules/devices/kostal_piko_old/device.py new file mode 100644 index 000000000..ca1e0c41a --- /dev/null +++ b/packages/modules/devices/kostal_piko_old/device.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import logging +from typing import Iterable, Optional, List + +from helpermodules.cli import run_using_positional_cli_args +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, MultiComponentUpdater +from modules.devices.kostal_piko_old import inverter +from modules.devices.kostal_piko_old.config import KostalPikoOld, KostalPikoOldConfiguration, KostalPikoOldInverterSetup +from modules.devices.kostal_piko_old.inverter import KostalPikoOldInverter + +log = logging.getLogger(__name__) + + +def create_device(device_config: KostalPikoOld): + def create_inverter_component(component_config: KostalPikoOldInverterSetup): + return KostalPikoOldInverter(component_config) + + def update_components(components: Iterable[KostalPikoOldInverter]): + response = req.get_http_session().get(device_config.configuration.ip_address, verify=False, auth=( + device_config.configuration.user, device_config.configuration.password), timeout=5).text + for component in components: + component.update(response) + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + inverter=create_inverter_component, + ), + component_updater=MultiComponentUpdater(update_components) + ) + + +COMPONENT_TYPE_TO_MODULE = { + "inverter": inverter +} + + +def read_legacy(component_type: str, ip_address: str, user: str, password: str, num: Optional[int]) -> None: + device_config = KostalPikoOld( + configuration=KostalPikoOldConfiguration(ip_address=ip_address, user=user, password=password)) + dev = create_device(device_config) + if component_type in COMPONENT_TYPE_TO_MODULE: + component_config = COMPONENT_TYPE_TO_MODULE[component_type].component_descriptor.configuration_factory() + else: + raise Exception( + "illegal component type " + component_type + ". Allowed values: " + + ','.join(COMPONENT_TYPE_TO_MODULE.keys()) + ) + component_config.id = num + dev.add_component(component_config) + + log.debug('KostalPikoOld IP-Adresse: ' + ip_address) + log.debug('KostalPikoOld user: ' + user) + log.debug('KostalPikoOld Passwort: ' + password) + + dev.update() + + +def main(argv: List[str]): + run_using_positional_cli_args(read_legacy, argv) + + +device_descriptor = DeviceDescriptor(configuration_factory=KostalPikoOld) diff --git a/packages/modules/devices/kostal_piko_old/inverter.py b/packages/modules/devices/kostal_piko_old/inverter.py new file mode 100644 index 000000000..b504c7db5 --- /dev/null +++ b/packages/modules/devices/kostal_piko_old/inverter.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import logging +import re + +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo +from modules.common.store import get_inverter_value_store +from modules.devices.kostal_piko_old.config import KostalPikoOldInverterSetup + +log = logging.getLogger(__name__) + + +class KostalPikoOldInverter: + def __init__(self, component_config: KostalPikoOldInverterSetup) -> None: + self.component_config = component_config + self.store = get_inverter_value_store(self.component_config.id) + self.component_info = ComponentInfo.from_component_config(self.component_config) + + def update(self, response) -> None: + # power may be a string "xxx" when the inverter is offline, so we cannot match as a number + # state is just for debugging currently known states: + # - Aus + # - Leerlauf + result = re.search( + r"aktuell\s*]*>\s*([^<]+).*" + r"Gesamtenergie\s*]*>\s*(\d+).*" + r"Status\s*]*>\s*([^<]+)", + response, + re.DOTALL + ) + if result is None: + raise Exception("Given HTML does not match the expected regular expression. Ignoring.") + log.debug("Inverter data: state=%s, power=%s, exported=%s" % + (result.group(3), result.group(1), result.group(2))) + try: + power = -int(result.group(1)) + except ValueError: + log.info("Inverter power is not a number! Inverter may be offline. Setting power to 0 W.") + power = 0 + inverter_state = InverterState( + exported=int(result.group(2)) * 1000, + power=power + ) + self.store.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=KostalPikoOldInverterSetup) diff --git a/packages/modules/devices/kostal_piko_old/inverter_test.py b/packages/modules/devices/kostal_piko_old/inverter_test.py new file mode 100644 index 000000000..2c6c5deec --- /dev/null +++ b/packages/modules/devices/kostal_piko_old/inverter_test.py @@ -0,0 +1,28 @@ +from pathlib import Path +from unittest.mock import Mock + +import pytest +from modules.common.component_state import InverterState + +from modules.devices.kostal_piko_old import inverter +from modules.devices.kostal_piko_old.config import KostalPikoOldInverterSetup + + +@pytest.mark.parametrize("sample_file_name, expected_inverter_state", + [pytest.param("sample.html", InverterState(power=-50, exported=73288000), id="Inverter on"), + pytest.param("sample_off.html", InverterState( + power=0, exported=42906000), id="Inverter off")] + ) +def test_parse_html(sample_file_name, expected_inverter_state, monkeypatch): + # setup + sample = (Path(__file__).parent / sample_file_name).read_text() + mock_inverter_value_store = Mock() + monkeypatch.setattr(inverter, 'get_inverter_value_store', Mock(return_value=mock_inverter_value_store)) + inv = inverter.KostalPikoOldInverter(KostalPikoOldInverterSetup()) + + # execution + inv.update(sample) + + # evaluation + assert mock_inverter_value_store.set.call_count == 1 + assert vars(mock_inverter_value_store.set.call_args[0][0]) == vars(expected_inverter_state) diff --git a/modules/wr_kostalpikovar2/kostal_piko_var2_test_sample.html b/packages/modules/devices/kostal_piko_old/sample.html similarity index 100% rename from modules/wr_kostalpikovar2/kostal_piko_var2_test_sample.html rename to packages/modules/devices/kostal_piko_old/sample.html diff --git a/modules/wr_kostalpikovar2/kostal_piko_var2_test_sample_off.html b/packages/modules/devices/kostal_piko_old/sample_off.html similarity index 100% rename from modules/wr_kostalpikovar2/kostal_piko_var2_test_sample_off.html rename to packages/modules/devices/kostal_piko_old/sample_off.html diff --git a/packages/modules/devices/kostal_steca/inverter.py b/packages/modules/devices/kostal_steca/inverter.py index 6e8d226c2..4c23790ee 100644 --- a/packages/modules/devices/kostal_steca/inverter.py +++ b/packages/modules/devices/kostal_steca/inverter.py @@ -3,7 +3,6 @@ from typing import Optional, Tuple import xml.etree.ElementTree as ET import re -from math import isnan from modules.common import req from modules.common.component_state import InverterState @@ -38,9 +37,8 @@ def get_values(self) -> Tuple[float, Optional[float]]: # call for XML file and parse it for current PV power measurements = req.get_http_session().get("http://" + self.ip_address + "/measurements.xml", timeout=2).text - power = float(ET.fromstring(measurements).find( - ".//Measurement[@Type='AC_Power']").get("Value")) * -1 - power = 0 if isnan(power) else power + power_raw = ET.fromstring(measurements).find(".//Measurement[@Type='AC_Power']").get("Value") + power = 0 if power_raw is None else float(power_raw) * -1 if self.component_config.configuration.variant_steca: # call for XML file and parse it for total produced kwh diff --git a/packages/modules/devices/kostal_steca/inverter_test.py b/packages/modules/devices/kostal_steca/inverter_test.py index 2395fc614..1f2c5c35a 100644 --- a/packages/modules/devices/kostal_steca/inverter_test.py +++ b/packages/modules/devices/kostal_steca/inverter_test.py @@ -1,14 +1,21 @@ +import pytest + from modules.devices.kostal_steca.config import KostalStecaInverterSetup from modules.devices.kostal_steca.inverter import KostalStecaInverter SAMPLE_IP = "1.1.1.1" -def test_get_values(requests_mock): +@pytest.mark.parametrize("measurements_file, expected_power", + [ + pytest.param("measurements_production.xml", -132.8, id="WR produziert"), + pytest.param("measurements_no_production.xml", 0, id="WR produziert nicht"), + ]) +def test_get_values(measurements_file, expected_power, requests_mock): # setup inverter = KostalStecaInverter(KostalStecaInverterSetup(), SAMPLE_IP) - with open("packages/modules/devices/kostal_steca/measurements.xml", "r") as f: + with open("packages/modules/devices/kostal_steca/"+measurements_file, "r") as f: measurements_sample = f.read() requests_mock.get("http://" + SAMPLE_IP + "/measurements.xml", text=measurements_sample) @@ -20,5 +27,5 @@ def test_get_values(requests_mock): power, exported = inverter.get_values() # evaluation - assert power == -132.8 + assert power == expected_power assert exported == 12306056 diff --git a/packages/modules/devices/kostal_steca/measurements_no_production.xml b/packages/modules/devices/kostal_steca/measurements_no_production.xml new file mode 100644 index 000000000..10e2abaf3 --- /dev/null +++ b/packages/modules/devices/kostal_steca/measurements_no_production.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/modules/devices/kostal_steca/measurements.xml b/packages/modules/devices/kostal_steca/measurements_production.xml similarity index 100% rename from packages/modules/devices/kostal_steca/measurements.xml rename to packages/modules/devices/kostal_steca/measurements_production.xml diff --git a/packages/modules/devices/rct/bat.py b/packages/modules/devices/rct/bat.py index 0aaf5dbc4..9f87b1b8a 100644 --- a/packages/modules/devices/rct/bat.py +++ b/packages/modules/devices/rct/bat.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 +import logging + from dataclass_utils import dataclass_from_dict from modules.common.component_state import BatState from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.fault_state import ComponentInfo from modules.common.store import get_bat_value_store from modules.devices.rct.config import RctBatSetup from modules.devices.rct.rct_lib import RCT +log = logging.getLogger(__name__) + class RctBat: def __init__(self, component_config: RctBatSetup) -> None: @@ -27,9 +31,6 @@ def update(self, rct_client: RCT) -> None: # read all parameters rct_client.read(my_tab) - if (stat1.value + stat2.value + stat3.value) > 0: - raise FaultState.error("Alarm Status Speicher ist ungleich 0.") - bat_state = BatState( power=watt1.value * -1, soc=socx.value * 100, @@ -37,6 +38,11 @@ def update(self, rct_client: RCT) -> None: exported=watt3.value ) self.store.set(bat_state) + if (stat1.value + stat2.value + stat3.value) > 0: + # Werte werden trotz Fehlercode übermittelt. + log.warning( + "Alarm Status Speicher ist ungleich 0. Status 1: " + str(stat1.value) + ", Status 2: " + + str(stat2.value) + ", Status 3: " + str(stat3.value)) component_descriptor = ComponentDescriptor(configuration_factory=RctBatSetup) diff --git a/packages/modules/devices/rct/counter.py b/packages/modules/devices/rct/counter.py index 49c654221..c0bb88a49 100644 --- a/packages/modules/devices/rct/counter.py +++ b/packages/modules/devices/rct/counter.py @@ -1,12 +1,16 @@ #!/usr/bin/env python3 +import logging + from dataclass_utils import dataclass_from_dict from modules.common.component_state import CounterState from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.fault_state import ComponentInfo from modules.common.store import get_counter_value_store from modules.devices.rct.config import RctCounterSetup from modules.devices.rct.rct_lib import RCT +log = logging.getLogger(__name__) + class RctCounter: def __init__(self, component_config: RctCounterSetup) -> None: @@ -35,9 +39,6 @@ def update(self, rct_client: RCT): # read all parameters rct_client.read(my_tab) - if (stat1.value + stat2.value + stat3.value + stat4.value) > 0: - raise FaultState.error("Alarm Status Zähler ist ungleich 0.") - counter_state = CounterState( imported=imported.value, exported=exported.value*-1.0, @@ -47,6 +48,11 @@ def update(self, rct_client: RCT): voltages=[volt1.value, volt2.value, volt3.value] ) self.store.set(counter_state) + if (stat1.value + stat2.value + stat3.value + stat4.value) > 0: + # Werte werden trotz Fehlercode übermittelt. + log.warning( + "Alarm Status Speicher ist ungleich 0. Status 1: " + str(stat1.value) + " Status 2: " + + str(stat2.value) + ", Status 3: " + str(stat3.value) + ", Status 4: " + str(stat4.value)) component_descriptor = ComponentDescriptor(configuration_factory=RctCounterSetup) diff --git a/packages/modules/devices/rct/rct_lib.py b/packages/modules/devices/rct/rct_lib.py index 1599e1bb1..9023698e5 100755 --- a/packages/modules/devices/rct/rct_lib.py +++ b/packages/modules/devices/rct/rct_lib.py @@ -66,6 +66,34 @@ class rct_data(Enum): t_dump = 12 +# battery status definitions (same for battery.status and status2) +# battery.status2: comes from BMS +# battery.status: comes from Inverter +class battery_status(Enum): + t_bat_status_disconnected = (1 << 0) # Battery disconnected + t_bat_status_relay_test = (1 << 1) + t_bat_status_ready = (1 << 2) + t_bat_status_calibration = (1 << 3) # Calibration at high voltage + t_bat_status_standby = (1 << 4) # Battery standby (connected) / bootloader + t_bat_status_shut_down = (1 << 5) # Shut Down + t_bat_status_precharge = (1 << 6) # Precharge + t_bat_status_startup = (1 << 7) # Startup + t_bat_status_battery_full = (1 << 8) # Battery is full + t_bat_status_battery_empty = (1 << 9) # Battery is empty + t_bat_status_calibration_empty = (1 << 10) # Calibration at low voltage + t_bat_status_balance = (1 << 11) # Balancing active + t_bat_status_error = (1 << 12) # Error + t_bat_status_update = (1 << 13) # Update running + + +# battery.bat_status definitions +class battery_bat_status(Enum): + t_bc_result_disconnected = 0 # Battery is disconnected + t_bc_result_connecting_no_pump = 1 # Battery in connecting state, but DC-Link should not be pumped + t_bc_result_connecting = 2 # Battery in connecting state and DC-Link should be pumped + t_bc_result_connected = 3 # Battery is connected + + class rct_id(): def __init__(self, msgid, idx, name, data_type=rct_data.t_unknown, desc=''): self.id = msgid diff --git a/packages/modules/devices/smart_me/counter.py b/packages/modules/devices/smart_me/counter.py index 3e83224e4..d83f3012c 100644 --- a/packages/modules/devices/smart_me/counter.py +++ b/packages/modules/devices/smart_me/counter.py @@ -23,7 +23,7 @@ def __init__(self, def update(self, session: Session) -> None: def parse_phase_values(key: str) -> List[float]: - return [response[key+str(i)] * 1000 for i in range(1, 4)] + return [response[key+str(i)] for i in range(1, 4)] response = session.get('https://smart-me.com:443/api/Devices/' + self.component_config.configuration.id, timeout=3).json() @@ -33,6 +33,7 @@ def parse_phase_values(key: str) -> List[float]: currents[0] = response["Current"] powers = parse_phase_values("ActivePowerL") + powers = [powers[i] * 1000 for i in range(0, 3)] if powers[0] == 0: powers[0] = response["ActivePower"] diff --git a/packages/modules/devices/solar_log/device.py b/packages/modules/devices/solar_log/device.py index d18b72e18..b3d799d16 100644 --- a/packages/modules/devices/solar_log/device.py +++ b/packages/modules/devices/solar_log/device.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import json import logging -from typing import List, Optional, Union +from typing import Iterable, List, Optional, Union from helpermodules.cli import run_using_positional_cli_args from modules.common.abstract_device import DeviceDescriptor @@ -21,7 +21,7 @@ def create_counter_component(component_config: SolarLogCounterSetup): def create_inverter_component(component_config: SolarLogInverterSetup): return SolarLogInverter(device_config.id, component_config) - def update_components(components: Union[SolarLogCounter, SolarLogInverter]): + def update_components(components: Iterable[Union[SolarLogCounter, SolarLogInverter]]): response = req.get_http_session().post('http://'+device_config.ip_adress+'/getjp', data=json.dumps({"801": {"170": None}}), timeout=5).json() for component in components: diff --git a/packages/modules/devices/solar_watt/device.py b/packages/modules/devices/solar_watt/device.py index ef6efe975..f2c260d91 100644 --- a/packages/modules/devices/solar_watt/device.py +++ b/packages/modules/devices/solar_watt/device.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import logging -from typing import Dict, List, Optional, Union +from typing import Dict, Iterable, List, Optional, Union from helpermodules.cli import run_using_positional_cli_args from modules.common.abstract_device import DeviceDescriptor @@ -15,7 +15,7 @@ log = logging.getLogger(__name__) -def update(components: Dict[str, Union[SolarWattBat, SolarWattCounter, SolarWattInverter]], +def update(components: Iterable[Union[SolarWattBat, SolarWattCounter, SolarWattInverter]], energy_manager: bool, ip_adress: Optional[str] = None ): @@ -30,7 +30,7 @@ def request(url: str) -> Dict: energy_manager_response = request('http://'+ip_adress + '/rest/kiwigrid/wizard/devices') else: gateway_response = request('http://'+ip_adress+':8080/') - for component in components.values(): + for component in components: if isinstance(component, SolarWattInverter): if energy_manager_response is None: energy_manager_response = request('http://'+ip_adress + '/rest/kiwigrid/wizard/devices') diff --git a/packages/modules/devices/solaredge/device.py b/packages/modules/devices/solaredge/device.py index 4e836912a..95e5979c4 100644 --- a/packages/modules/devices/solaredge/device.py +++ b/packages/modules/devices/solaredge/device.py @@ -2,6 +2,7 @@ import logging from operator import add from statistics import mean +import time from typing import Dict, Iterable, Tuple, Union, Optional, List from urllib3.util import parse_url @@ -30,6 +31,8 @@ solaredge_component_classes = Union[SolaredgeBat, SolaredgeCounter, SolaredgeExternalInverter, SolaredgeInverter] default_unit_id = 85 +synergy_unit_identifier = 160 +reconnect_delay = 1.2 class Device(AbstractDevice): @@ -68,10 +71,30 @@ def add_component(self, self.device_config.id, component_config, self.client)) if component_type == "inverter" or component_type == "external_inverter": self.inverter_counter += 1 - self.synergy_units = int(self.client.read_holding_registers( - 40129, modbus.ModbusDataType.UINT_16, - unit=component_config.configuration.modbus_id)) or 1 - log.debug("Synergy Units: %s", self.synergy_units) + # try: + # # ToDo: convert to String + # manufacturer = self.client.read_holding_registers(40004, [modbus.ModbusDataType.UINT_32]*8, + # unit=component_config.configuration.modbus_id) + # # ToDo: convert to String + # model = self.client.read_holding_registers(40020, [modbus.ModbusDataType.UINT_32]*8, + # unit=component_config.configuration.modbus_id) + # # ToDo: convert to String + # version = self.client.read_holding_registers(40044, [modbus.ModbusDataType.UINT_16]*8, + # unit=component_config.configuration.modbus_id) + # serial_number = self.client.read_holding_registers(40052, [modbus.ModbusDataType.UINT_32]*8, + # unit=component_config.configuration.modbus_id) + # log.debug("Version: " + str(version)) + # except Exception as e: + # log.exception("Fehler beim Auslesen der Modbus-Register: " + str(e)) + # pass + if self.client.read_holding_registers(40121, modbus.ModbusDataType.UINT_16, + unit=component_config.configuration.modbus_id + ) == synergy_unit_identifier: + log.debug("Synergy Units supported") + self.synergy_units = int(self.client.read_holding_registers( + 40129, modbus.ModbusDataType.UINT_16, + unit=component_config.configuration.modbus_id)) or 1 + log.debug("Synergy Units detected: %s", self.synergy_units) if component_type == "external_inverter" or component_type == "counter" or component_type == "inverter": self.set_component_registers(self.components.values(), self.synergy_units) else: @@ -232,13 +255,13 @@ def create_inverter(modbus_id: int) -> SolaredgeInverter: total_power = state.power total_energy = state.exported - if batwrsame == 1: - zweiterspeicher = 0 - bat_power, bat_state = get_bat_state() - if state.dc_power is None or state.dc_power <= 0: - total_power -= sum(bat_power) - total_energy = total_energy + bat_state.imported - bat_state.exported - get_bat_value_store(1).set(bat_state) + if batwrsame == 1: + zweiterspeicher = 0 + bat_power, bat_state = get_bat_state() + if state.dc_power is None or state.dc_power <= 0: + total_power -= sum(bat_power) + total_energy = total_energy + bat_state.imported - bat_state.exported + get_bat_value_store(1).set(bat_state) device_config = Solaredge(configuration=SolaredgeConfiguration(ip_address=ip2address)) dev = Device(device_config) inv = create_inverter(int(slave_id0)) @@ -250,6 +273,7 @@ def create_inverter(modbus_id: int) -> SolaredgeInverter: state = get_external_inverter_state(dev, int(slave_id0)) total_power += state.power get_inverter_value_store(num).set(InverterState(exported=total_energy, power=total_power)) + time.sleep(reconnect_delay) elif component_type == "bat": with SingleComponentUpdateContext(ComponentInfo(0, "Solaredge Speicher", "bat")): diff --git a/packages/modules/devices/sungrow/config.py b/packages/modules/devices/sungrow/config.py index fd2268c0e..2fb6a668c 100644 --- a/packages/modules/devices/sungrow/config.py +++ b/packages/modules/devices/sungrow/config.py @@ -4,8 +4,9 @@ class SungrowConfiguration: - def __init__(self, ip_address: Optional[str] = None, modbus_id: int = 1): + def __init__(self, ip_address: Optional[str] = None, port: int = 502, modbus_id: int = 1): self.ip_address = ip_address + self.port = port self.modbus_id = modbus_id diff --git a/packages/modules/devices/sungrow/device.py b/packages/modules/devices/sungrow/device.py index fbc2089ea..be3de2246 100644 --- a/packages/modules/devices/sungrow/device.py +++ b/packages/modules/devices/sungrow/device.py @@ -31,7 +31,8 @@ def __init__(self, device_config: Union[Dict, Sungrow]) -> None: try: self.device_config = dataclass_from_dict(Sungrow, device_config) ip_address = self.device_config.configuration.ip_address - self.client = modbus.ModbusTcpClient_(ip_address, 502) + port = self.device_config.configuration.port + self.client = modbus.ModbusTcpClient_(ip_address, port) except Exception: log.exception("Fehler im Modul " + self.device_config.name) @@ -82,34 +83,36 @@ def update(self) -> None: def read_legacy(ip_address: str, + port: int, modbus_id: int, component_config: dict): device_config = Sungrow() device_config.configuration.ip_address = ip_address - device_config.configuration.port = 502 + device_config.configuration.port = port device_config.configuration.modbus_id = modbus_id dev = Device(device_config) dev.add_component(component_config) dev.update() -def read_legacy_bat(ip_address: str, modbus_id: int): - read_legacy(ip_address, modbus_id, bat.component_descriptor.configuration_factory(id=None)) +def read_legacy_bat(ip_address: str, port: int, modbus_id: int): + read_legacy(ip_address, port, modbus_id, bat.component_descriptor.configuration_factory(id=None)) -def read_legacy_counter(ip_address: str, modbus_id: int, version: int): - read_legacy(ip_address, modbus_id, counter.component_descriptor.configuration_factory( +def read_legacy_counter(ip_address: str, port: int, modbus_id: int, version: int): + read_legacy(ip_address, port, modbus_id, counter.component_descriptor.configuration_factory( id=None, configuration=SungrowCounterConfiguration(version=Version(version)))) def read_legacy_inverter(ip_address: str, + port: int, modbus_id: int, num: int, read_counter: int, version: int): device_config = Sungrow() device_config.configuration.ip_address = ip_address - device_config.configuration.port = 502 + device_config.configuration.port = port device_config.configuration.modbus_id = modbus_id dev = Device(device_config) dev.add_component(inverter.component_descriptor.configuration_factory(id=num)) diff --git a/packages/modules/devices/varta/counter.py b/packages/modules/devices/varta/counter.py index a5de2503a..73e7efe0d 100644 --- a/packages/modules/devices/varta/counter.py +++ b/packages/modules/devices/varta/counter.py @@ -19,14 +19,12 @@ def __init__(self, device_id: int, component_config: VartaCounterSetup) -> None: def update(self, client: ModbusTcpClient_): power = client.read_holding_registers(1078, ModbusDataType.INT_16, unit=1) * -1 - frequency = client.read_holding_registers(1082, ModbusDataType.UINT_16, unit=1) / 100 imported, exported = self.sim_counter.sim_count(power) counter_state = CounterState( imported=imported, exported=exported, power=power, - frequency=frequency ) self.store.set(counter_state) diff --git a/packages/modules/internal_chargepoint_handler/chargepoint_module.py b/packages/modules/internal_chargepoint_handler/chargepoint_module.py index aea0a475f..035c5f0a3 100644 --- a/packages/modules/internal_chargepoint_handler/chargepoint_module.py +++ b/packages/modules/internal_chargepoint_handler/chargepoint_module.py @@ -34,12 +34,14 @@ def set_current(self, current: float) -> None: def get_values(self, phase_switch_cp_active: bool) -> Tuple[ChargepointState, float]: try: - _, power = self.__client.meter_client.get_power() + powers, power = self.__client.meter_client.get_power() if power < self.PLUG_STANDBY_POWER_THRESHOLD: power = 0 voltages = self.__client.meter_client.get_voltages() currents = self.__client.meter_client.get_currents() imported = self.__client.meter_client.get_imported() + power_factors = self.__client.meter_client.get_power_factors() + frequency = self.__client.meter_client.get_frequency() phases_in_use = sum(1 for current in currents if current > 3) time.sleep(0.1) @@ -63,12 +65,13 @@ def get_values(self, phase_switch_cp_active: bool) -> Tuple[ChargepointState, fl currents=currents, imported=imported, exported=0, - # powers=powers, + powers=powers, voltages=voltages, - # frequency=frequency, + frequency=frequency, plug_state=plug_state, charge_state=charge_state, phases_in_use=phases_in_use, + power_factors=power_factors, rfid=rfid ) except Exception as e: diff --git a/packages/modules/internal_chargepoint_handler/clients.py b/packages/modules/internal_chargepoint_handler/clients.py index 346959bb4..e1a808dd4 100644 --- a/packages/modules/internal_chargepoint_handler/clients.py +++ b/packages/modules/internal_chargepoint_handler/clients.py @@ -96,7 +96,14 @@ def client_factory(local_charge_point_num: int, resolved_devices = [str(file.resolve()) for file in tty_devices] log.debug("resolved_devices"+str(resolved_devices)) counter = len(resolved_devices) - if counter == 1 and resolved_devices[0] in BUS_SOURCES: + if counter == 0: + # Wenn kein USB-Gerät gefunden wird, wird der Modbus-Anschluss der AddOn-Platine genutzt (/dev/serial0) + serial_client = ModbusSerialClient_("/dev/serial0") + if local_charge_point_num == 0: + evse_ids = EVSE_ID_CP0 + else: + evse_ids = EVSE_ID_ONE_BUS_CP1 + elif counter == 1 and resolved_devices[0] in BUS_SOURCES: if local_charge_point_num == 0: log.error("LP0 Device: "+str(resolved_devices[0])) serial_client = ModbusSerialClient_(resolved_devices[0]) diff --git a/packages/modules/smarthome/acthor/smartacthor.py b/packages/modules/smarthome/acthor/smartacthor.py index 7324a29ca..49910f410 100644 --- a/packages/modules/smarthome/acthor/smartacthor.py +++ b/packages/modules/smarthome/acthor/smartacthor.py @@ -44,7 +44,8 @@ def getwatt(self, uberschuss: int, uberschussoffset: int) -> None: argumentList = ['python3', self._prefixpy + 'acthor/watt.py', str(self.device_nummer), str(self._device_ip), str(self.devuberschuss), self._device_acthortype, - self._device_acthorpower, str(forcesend)] + self._device_acthorpower, str(forcesend), + str(self.newwatt), str(self._oldmeasuretype1)] try: self.callpro(argumentList) self.answer = self.readret() diff --git a/packages/modules/smarthome/acthor/watt.py b/packages/modules/smarthome/acthor/watt.py index b482ec5ee..06308cb0f 100644 --- a/packages/modules/smarthome/acthor/watt.py +++ b/packages/modules/smarthome/acthor/watt.py @@ -14,6 +14,8 @@ atype = str(sys.argv[4]) instpower = int(sys.argv[5]) forcesend = int(sys.argv[6]) +aktpoweralt = int(sys.argv[7]) +measuretyp = str(sys.argv[8]) # forcesend = 0 default time period applies # forcesend = 1 default overwritten send now # forcesend = 9 default overwritten no send @@ -42,13 +44,23 @@ if instpower == 0: instpower = 1000 cap = 9000 -if atype == "9s18": +if atype == "9s45": + faktor = 45000/instpower + cap = 45000 +elif atype == "9s27": + faktor = 27000/instpower + cap = 27000 +elif atype == "9s18": faktor = 18000/instpower cap = 18000 elif atype == "9s": faktor = 9000/instpower elif atype == "M3": faktor = 6000/instpower +elif atype == "E2M1": + faktor = 3500/instpower +elif atype == "E2M3": + faktor = 6500/instpower else: faktor = 3000/instpower pvmodus = 0 @@ -67,10 +79,17 @@ value1 = resp.registers[0] all = format(value1, '04x') aktpower = int(struct.unpack('>h', codecs.decode(all, 'hex'))[0]) +# sofern externe Messung wird dieser Wert genommen +if measuretyp == 'empty': + aktpower = int(struct.unpack('>h', codecs.decode(all, 'hex'))[0]) +else: + aktpower = aktpoweralt # Wassertemperatur lesen # Temp0 Warmwasser 1001 # Temp1 1030 <- Optional wenn 0, nicht angeschlossen dann ersetzt durch 300 (keine Anzeige) # Temp2 1031 <- Optional wenn 0, nicht angeschlossen dann ersetzt durch 300 (keine Anzeige) +# elwa2 hat nur zwei temp Fuehler +# nicht drei value1 = resp.registers[1] all = format(value1, '04x') temp0int = int(struct.unpack('>h', codecs.decode(all, 'hex'))[0]) @@ -81,10 +100,13 @@ temp1 = temp1int / 10 if temp1 == 0: temp1 = 300 -value1 = resp.registers[31] -all = format(value1, '04x') -temp2int = int(struct.unpack('>h', codecs.decode(all, 'hex'))[0]) -temp2 = temp2int / 10 +if (atype == "E2M3" or atype == "E2M1"): + temp2 = 300.0 +else: + value1 = resp.registers[31] + all = format(value1, '04x') + temp2int = int(struct.unpack('>h', codecs.decode(all, 'hex'))[0]) + temp2 = temp2int / 10 if temp2 == 0: temp2 = 300 if count5 == 0: @@ -103,9 +125,11 @@ neupowertarget = int((uberschuss + aktpower) * faktor) if neupowertarget < 0: neupowertarget = 0 + if instpower > cap: + cap = instpower if neupowertarget > int(cap * faktor): neupowertarget = int(cap * faktor) - # status nach handbuch Thor + # status nach handbuch Thor/elwa2 # 0.. Aus # 1-8 Geraetestart # 9 Betrieb @@ -131,8 +155,8 @@ f.write(str(count1)) # mehr log schreiben if count1 < 3: - log.info(" watt devicenr %d ipadr %s ueberschuss %6d Akt Leistung %6d Status %2d" % - (devicenumber, ipadr, uberschuss, aktpower, status)) + log.info(" watt devicenr %d ipadr %s ueberschuss %6d Akt Leistung %6d Status %2d Externe Messung %s" % + (devicenumber, ipadr, uberschuss, aktpower, status, measuretyp)) log.info(" watt devicenr %d ipadr %s Neu Leistung %6d pvmodus %1d modbuswrite %1d" % (devicenumber, ipadr, neupower, pvmodus, modbuswrite)) log.info(" watt devicenr %d ipadr %s type %s inst. Leistung %6d Skalierung %.2f" % diff --git a/packages/modules/smarthome/idm/smartidm.py b/packages/modules/smarthome/idm/smartidm.py index dc6afd334..1c1163938 100644 --- a/packages/modules/smarthome/idm/smartidm.py +++ b/packages/modules/smarthome/idm/smartidm.py @@ -11,6 +11,8 @@ def __init__(self) -> None: super().__init__() self._device_idmnav = '2' self.device_nummer = 0 + self._device_idmueb = 'UZ' + self._device_maxueb = 0 def updatepar(self, input_param: Dict[str, str]) -> None: super().updatepar(input_param) @@ -18,10 +20,18 @@ def updatepar(self, input_param: Dict[str, str]) -> None: self.device_nummer = int(self._smart_paramadd.get('device_nummer', '0')) for key, value in self._smart_paramadd.items(): + try: + valueint = int(value) + except Exception: + valueint = 0 if (key == 'device_nummer'): pass elif (key == 'device_idmnav'): self._device_idmnav = value + elif (key == 'device_idmueb'): + self._device_idmueb = value + elif (key == 'device_maxueb'): + self._device_maxueb = valueint else: log.info("(" + str(self.device_nummer) + ") " + " IDM überlesen " + key + @@ -33,7 +43,10 @@ def getwatt(self, uberschuss: int, uberschussoffset: int) -> None: argumentList = ['python3', self._prefixpy + 'idm/watt.py', str(self.device_nummer), str(self._device_ip), str(self.devuberschuss), str(self._device_idmnav), - str(self.pvwatt), str(forcesend)] + str(self.pvwatt), + str(self._device_idmueb), + str(self._device_maxueb), + str(forcesend)] try: self.callpro(argumentList) self.answer = self.readret() diff --git a/packages/modules/smarthome/idm/watt.py b/packages/modules/smarthome/idm/watt.py index 52004b530..bb4a1c8a0 100644 --- a/packages/modules/smarthome/idm/watt.py +++ b/packages/modules/smarthome/idm/watt.py @@ -13,7 +13,9 @@ uberschuss = int(sys.argv[3]) navvers = str(sys.argv[4]) pvwatt = int(sys.argv[5]) -forcesend = int(sys.argv[6]) +uberschussvz = str(sys.argv[6]) +maxpower = int(sys.argv[7]) +forcesend = int(sys.argv[8]) # forcesend = 0 default time period applies # forcesend = 1 default overwritten send now # forcesend = 9 default overwritten no send @@ -37,12 +39,28 @@ count5 = 0 with open(file_stringcount5, 'w') as f: f.write(str(count5)) +# aktuelle Leistung lesen +client = ModbusTcpClient(ipadr, port=502) +# test +# start = 3501 +# navvers = "2" +# prod +start = 4122 +if navvers == "2": + rr = client.read_input_registers(start, 2, unit=1) +else: + rr = client.read_holding_registers(start, 2, unit=1) +raw = struct.pack('>HH', rr.getRegister(1), rr.getRegister(0)) +lkw = float(struct.unpack('>f', raw)[0]) +aktpower = int(lkw*1000) +modbuswrite = 0 +neupower = 0 +# pv modus +pvmodus = 0 +if os.path.isfile(file_stringpv): + with open(file_stringpv, 'r') as f: + pvmodus = int(f.read()) if count5 == 0: - # pv modus - pvmodus = 0 - if os.path.isfile(file_stringpv): - with open(file_stringpv, 'r') as f: - pvmodus = int(f.read()) # log counter count1 = 999 if os.path.isfile(file_stringcount): @@ -53,30 +71,28 @@ count1 = 0 with open(file_stringcount, 'w') as f: f.write(str(count1)) - # aktuelle Leistung lesen - client = ModbusTcpClient(ipadr, port=502) - # test - # start = 3501 - # navvers = "2" - # prod - start = 4122 - if navvers == "2": - rr = client.read_input_registers(start, 2, unit=1) - else: - rr = client.read_holding_registers(start, 2, unit=1) - raw = struct.pack('>HH', rr.getRegister(1), rr.getRegister(0)) - lkw = float(struct.unpack('>f', raw)[0]) - aktpower = int(lkw*1000) # logik nur schicken bei pvmodus - modbuswrite = 0 if pvmodus == 1: modbuswrite = 1 - # Nur positiven Uberschuss schicken, nicht aktuelle Leistung neupower = uberschuss - if neupower < 0: - neupower = 0 - if neupower > 40000: - neupower = 40000 + # uberschuss begrenzung ? + if (maxpower > 0): + neupower = maxpower - aktpower + # maximaler überschuss berechnet ? + if (neupower > uberschuss): + neupower = uberschuss + if (uberschussvz == 'UZ'): + # + # + if neupower < 0: + neupower = 0 + if neupower > 65535: + neupower = 65535 + else: + if neupower < -32767: + neupower = -32767 + if neupower > 32767: + neupower = 32767 # wurde IDM gerade ausgeschaltet ? (pvmodus == 99 ?) # dann 0 schicken wenn kein pvmodus mehr # und pv modus ausschalten @@ -101,19 +117,22 @@ pvwnew = builder.to_registers() # json return power = aktuelle Leistungsaufnahme in Watt, # on = 1 pvmodus, powerc = counter in kwh - answer = '{"power":' + str(aktpower) + ',"powerc":0' - answer += ',"send":' + str(modbuswrite) + ',"sendpower":' + str(neupower) - answer += ',"on":' + str(pvmodus) + '}' - writeret(answer, devicenumber) if count1 < 3: log.info(" %d ipadr %s ueberschuss %6d Akt Leistung %6d Pv %6d" % (devicenumber, ipadr, uberschuss, aktpower, pvwatt)) - log.info(" %d ipadr %s ueberschuss %6d pvmodus %1d modbusw %1d" + log.info(" %d ipadr %s ueberschuss send %6d pvmodus %1d modbusw %1d" % (devicenumber, ipadr, neupower, pvmodus, modbuswrite)) # modbus write if modbuswrite == 1: client.write_registers(74, regnew, unit=1) - client.write_registers(78, pvwnew, unit=1) if count1 < 3: log.info("devicenr %d ipadr %s device written by modbus " % (devicenumber, ipadr)) + client.write_registers(78, pvwnew, unit=1) +else: + if pvmodus == 99: + pvmodus = 0 +answer = '{"power":' + str(aktpower) + ',"powerc":0' +answer += ',"send":' + str(modbuswrite) + ',"sendpower":' + str(neupower) +answer += ',"on":' + str(pvmodus) + '}' +writeret(answer, devicenumber) diff --git a/packages/modules/smarthome/lambda_/smartlambda.py b/packages/modules/smarthome/lambda_/smartlambda.py index e5b187ba9..dbf23c4fb 100644 --- a/packages/modules/smarthome/lambda_/smartlambda.py +++ b/packages/modules/smarthome/lambda_/smartlambda.py @@ -15,7 +15,7 @@ def getwatt(self, uberschuss: int, uberschussoffset: int) -> None: argumentList = ['python3', self._prefixpy + 'lambda_/watt.py', str(self.device_nummer), str(self._device_ip), str(self.devuberschuss), str(self.device_lambdaueb), - str(forcesend)] + str(forcesend), str(self.pvwatt)] try: self.callpro(argumentList) self.answer = self.readret() diff --git a/packages/modules/smarthome/lambda_/watt.py b/packages/modules/smarthome/lambda_/watt.py index c50ebe8b1..85c97a9e3 100644 --- a/packages/modules/smarthome/lambda_/watt.py +++ b/packages/modules/smarthome/lambda_/watt.py @@ -5,10 +5,16 @@ import struct import codecs import logging -from pymodbus.payload import BinaryPayloadBuilder, Endian +from pymodbus.payload import BinaryPayloadBuilder from pymodbus.client.sync import ModbusTcpClient from smarthome.smartlog import initlog from smarthome.smartret import writeret +# fix for pymodbus endian class (changes once 2023 august to enum to uppercases only, +# checked during runtime, +# not compatible betwwen openwb 1.9 (want lowercases) and openwb 2.0 (wants upercase)) +auto = "@" +big = ">" +little = "<" named_tuple = time.localtime() # getstruct_time time_string = time.strftime("%m/%d/%Y, %H:%M:%S lambda watty.py", named_tuple) devicenumber = int(sys.argv[1]) @@ -16,6 +22,7 @@ uberschuss = int(sys.argv[3]) uberschussvz = str(sys.argv[4]) forcesend = int(sys.argv[5]) +pvwatt = int(sys.argv[6]) # forcesend = 0 default acthor time period applies # forcesend = 1 default overwritten send now # forcesend = 9 default overwritten no send @@ -72,6 +79,7 @@ modbuswrite = 1 neupower = uberschuss if (uberschussvz == 'UZ'): + neupower = pvwatt if neupower < 0: neupower = 0 if neupower > 65535: @@ -98,7 +106,7 @@ # modbus write if modbuswrite == 1: # andernfalls absturz bei negativen Zahlen - builder = BinaryPayloadBuilder(byteorder=Endian.Big) + builder = BinaryPayloadBuilder(byteorder=big) builder.reset() builder.add_16bit_int(neupower) pay = builder.to_registers() diff --git a/packages/modules/smarthome/nxdacxx/smartnxdacxx.py b/packages/modules/smarthome/nxdacxx/smartnxdacxx.py index a7715e845..5b73ec5ba 100644 --- a/packages/modules/smarthome/nxdacxx/smartnxdacxx.py +++ b/packages/modules/smarthome/nxdacxx/smartnxdacxx.py @@ -44,7 +44,8 @@ def getwatt(self, uberschuss: int, uberschussoffset: int) -> None: str(self.devuberschuss), str(self._device_nxdacxxueb), str(forcesend), str(self._device_dacport), - str(self._device_nxdacxxtype)] + str(self._device_nxdacxxtype), + str(self.newwatt)] try: self.callpro(argumentList) self.answer = self.readret() diff --git a/packages/modules/smarthome/nxdacxx/watt.py b/packages/modules/smarthome/nxdacxx/watt.py index cf98e89f7..17809ca98 100644 --- a/packages/modules/smarthome/nxdacxx/watt.py +++ b/packages/modules/smarthome/nxdacxx/watt.py @@ -12,6 +12,7 @@ forcesend = int(sys.argv[5]) port = int(sys.argv[6]) dactyp = int(sys.argv[7]) +aktpoweralt = int(sys.argv[8]) initlog("DAC", devicenumber) log = logging.getLogger("DAC") # forcesend = 0 default time period applies @@ -36,12 +37,15 @@ with open(file_stringcount5, 'w') as f: f.write(str(count5)) modbuswrite = 0 -neupower = uberschuss +if (dactyp == 3) or (dactyp == 1): + neupower = uberschuss + aktpoweralt +else: + neupower = uberschuss if neupower < 0: neupower = 0 if neupower > maxpower: neupower = maxpower -volt = 0 +ausgabe = 0 pvmodus = 0 if os.path.isfile(file_stringpv): with open(file_stringpv, 'r') as f: @@ -71,38 +75,44 @@ if count1 > 80: count1 = 0 if count1 < 3: - helpstr = 'devicenr %d ipadr %s ueberschuss %6d port %4d' + helpstr = 'devicenr %d ipadr %s ueberschuss %6d aktpoweralt %6d port %4d' helpstr += ' maxueberschuss %6d pvmodus %1d modbuswrite %1d' - log.info(helpstr % (devicenumber, ipadr, uberschuss, + log.info(helpstr % (devicenumber, ipadr, uberschuss, aktpoweralt, port, maxpower, pvmodus, modbuswrite)) # modbus write if modbuswrite == 1: client = ModbusTcpClient(ipadr, port=port) if dactyp == 0: # 10 Volts are 1000 - volt = int((neupower * 1000) / maxpower) - rq = client.write_register(1, volt, unit=1) + ausgabe = int((neupower * 1000) / maxpower) + rq = client.write_register(1, ausgabe, unit=1) elif dactyp == 1: - # 10 volts are 4000 - volt = int((neupower * 4000) / maxpower) - rq = client.write_register(0x01f4, volt, unit=1) + # 10 Volts are 4000 + ausgabe = int((neupower * 4000) / maxpower) + rq = client.write_register(0x01f4, ausgabe, unit=1) elif dactyp == 2: - volt = int((neupower * 4095) / maxpower) - if volt < 370: - volt = 370 + ausgabe = int((neupower * 4095) / maxpower) + if ausgabe < 370: + ausgabe = 370 # ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus - rq = client.write_register(0, volt, unit=1) + rq = client.write_register(0, ausgabe, unit=1) + elif dactyp == 3: + ausgabe = int(((neupower * (4095-820)) / maxpower)+820) + if ausgabe <= 820: + ausgabe = 0 + # ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus + rq = client.write_register(0x01f4, ausgabe, unit=1) else: pass if count1 < 3: - log.info('devicenr %d ipadr %s Volt %6d dactyp %d written by modbus ' % - (devicenumber, ipadr, volt, dactyp)) + log.info('devicenr %d ipadr %s Modbuswert %6d dactyp %d written by modbus ' % + (devicenumber, ipadr, ausgabe, dactyp)) with open(file_stringcount, 'w') as f: f.write(str(count1)) else: if pvmodus == 99: pvmodus = 0 answer = '{"power":' + str(aktpower) + ',"powerc":' + str(powerc) -answer += ',"send":' + str(modbuswrite) + ',"sendpower":' + str(volt) +answer += ',"send":' + str(modbuswrite) + ',"sendpower":' + str(ausgabe) answer += ',"on":' + str(pvmodus) + '}' writeret(answer, devicenumber) diff --git a/packages/modules/smarthome/shelly/off.py b/packages/modules/smarthome/shelly/off.py index d14b84253..e9ce1b4f4 100644 --- a/packages/modules/smarthome/shelly/off.py +++ b/packages/modules/smarthome/shelly/off.py @@ -26,16 +26,26 @@ gen = str(jsonin['gen']) model = str(jsonin['model']) else: - gen = "0" -if (chan == 0): - url = "http://" + str(ipadr) + "/relay/0?turn=off" -# urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=off", -# timeout=3) + gen = "1" +if (gen == "1"): + if (chan == 0): + url = "http://" + str(ipadr) + "/relay/0?turn=off" + # urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=off", + # timeout=3) + else: + chan = chan - 1 + url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=off" + # urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + + # "?turn=off", timeout=3) else: - chan = chan - 1 - url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=off" -# urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + -# "?turn=off", timeout=3) + if (chan > 0): + chan = chan - 1 + # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater + # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann + if ("SPEM-003CE" in model): + chan = 100 + # gen 2 will das als off cmd IPderPro3EM/rpc/Switch.Set?id=100&on=false + url = "http://" + str(ipadr) + "/rpc/Switch.Set?id=" + str(chan) + "&on=false" if (shaut == 1): # print("Shelly off" + str(shaut) + user + pw) passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() diff --git a/packages/modules/smarthome/shelly/on.py b/packages/modules/smarthome/shelly/on.py index bfaa4eb4a..85320a644 100644 --- a/packages/modules/smarthome/shelly/on.py +++ b/packages/modules/smarthome/shelly/on.py @@ -24,15 +24,25 @@ model = str(jsonin['model']) else: gen = "1" -if (chan == 0): - url = "http://" + str(ipadr) + "/relay/0?turn=on" -# urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=on", -# timeout=3) +if (gen == "1"): + if (chan == 0): + url = "http://" + str(ipadr) + "/relay/0?turn=on" + # urllib.request.urlopen("http://"+str(ipadr)+"/relay/0?turn=on", + # timeout=3) + else: + chan = chan - 1 + url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=on" + # urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + + # "?turn=on", timeout=3) else: - chan = chan - 1 - url = "http://" + str(ipadr) + "/relay/" + str(chan) + "?turn=on" -# urllib.request.urlopen("http://"+str(ipadr)+"/relay/" + str(chan) + -# "?turn=on", timeout=3) + if (chan > 0): + chan = chan - 1 + # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater + # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann + if ("SPEM-003CE" in model): + chan = 100 + # gen 2 will das als on cmd /rpc/Switch.Set?id=100&on=true + url = "http://" + str(ipadr) + "/rpc/Switch.Set?id=" + str(chan) + "&on=true" if (shaut == 1): # print("Shelly on" + str(shaut) + user + pw) passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() diff --git a/packages/modules/smarthome/shelly/watt.py b/packages/modules/smarthome/shelly/watt.py index 7218478a7..299ca52be 100644 --- a/packages/modules/smarthome/shelly/watt.py +++ b/packages/modules/smarthome/shelly/watt.py @@ -4,10 +4,11 @@ import time import json import urllib.request +from typing import Any from smarthome.smartret import writeret -def totalPowerFromShellyJson(answer, workchan: int) -> int: +def totalPowerFromShellyJson(answer: Any, workchan: int) -> int: if (workchan == 0): if 'meters' in answer: meters = answer['meters'] # shelly @@ -120,6 +121,9 @@ def totalPowerFromShellyJson(answer, workchan: int) -> int: aktpower = int(answer['em:0']['c_act_power']) else: aktpower = int(answer['em:0']['total_act_power']) + elif ("PM-001PCEU16" in model): + # "SNPM-001PCEU16" (gen 2) und "S3PM-001PCEU16" (gen 3) + aktpower = int(answer['pm1:0']['apower']) else: aktpower = int(answer[sw]['apower']) except Exception: @@ -133,6 +137,10 @@ def totalPowerFromShellyJson(answer, workchan: int) -> int: if (gen == "1"): relais = int(answer['relays'][workchan]['ison']) else: + # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater + # Leistunsmessung erfasst werden, da die Leistung auf drei verschieden Kanäle angeliefert werden kann + if ("SPEM-003CE" in model): + workchan = 100 sw = 'switch:' + str(workchan) relais = int(answer[sw]['output']) except Exception: diff --git a/packages/smarthome/smartbase.py b/packages/smarthome/smartbase.py index 5925a24cd..db423658e 100644 --- a/packages/smarthome/smartbase.py +++ b/packages/smarthome/smartbase.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 import time import os -from typing import Dict, Tuple, Any +from typing import Dict, Tuple from smarthome.smartbase0 import Sbase0 from smarthome.smartmeas import Slsdm630, Sllovato, Slsdm120, Slwe514, Slfronius from smarthome.smartmeas import Sljson, Slsmaem, Slshelly, Sltasmota, Slmqtt -from smarthome.smartmeas import Slhttp, Slavm, Slmystrom +from smarthome.smartmeas import Slhttp, Slavm, Slmystrom, Slb23 from smarthome.smartbut import Sbshelly from datetime import datetime, timezone import logging @@ -27,106 +27,6 @@ class Sbase(Sbase0): def __init__(self) -> None: # setting super().__init__() - self.mqtt_param = {} # type: Dict[str, str] - self.mqtt_param_del = {} # type: Dict[str, str] - self.device_name = 'none' - self.devstatus = 10 - # (10 = ueberschuss gesteuert oder manual, - # 20 = Anlauferkennung aktiv - # (ausschalten wenn Leistungsaufnahme > Schwelle) - # 30 = gestartet um fertig bis zu erreichen - # default 10 - self._first_run = 1 - self.device_nummer = 0 - self.temp0 = '300' - self.temp1 = '300' - self.temp2 = '300' - self.newwatt = 0 - self.newwattk = 0 - self.pvwatt = 0 - self.relais = 0 - self.devuberschuss = 0 - self.device_temperatur_configured = 0 - self.ueberschussberechnung = 1 - self.abschalt = 0 - self.device_homeconsumtion = 0 - self.device_manual = 0 - self.device_manual_control = 0 - self.newdevice_manual = 0 - self.newdevice_manual_control = 0 - self.device_type = 'none' - self._smart_param = {} # type: Dict[str, str] - self._uberschussoffset = 0 - self._uberschuss = 0 - self.device_canswitch = 0 - self._device_deactivatewhileevcharging = 0 - self._device_mineinschaltdauer = 0 - self._device_mindayeinschaltdauer = 0 - self._device_maxeinschaltdauer = 0 - self._device_differentmeasurement = 0 - self._device_speichersocbeforestop = 100 - self._device_speichersocbeforestart = 0 - self._device_startupdetection = 0 - self._device_standbypower = 0 - self._device_standbyduration = 0 - self._device_startupmuldetection = 0 - self._device_einschaltschwelle = 0 - self._device_ausschaltschwelle = 0 - self._device_einschaltverzoegerung = 0 - self._device_ausschaltverzoegerung = 0 - self._device_configured = '0' - self._device_ip = 'none' - self._device_measuretype = 'none' - self._device_measureip = 'none' - self._device_measureportsdm = 8899 - self._device_dacport = 8899 - self._device_measureid = 0 - self._device_finishtime = '00:00' - self._device_starttime = '00:00' - self._device_endtime = '00:00' - self._device_ontime = '00:00' - self._device_offtime = '00:00' - self._device_onuntiltime = '00:00' - self._device_nonewatt = 0 - self._device_deactivateper = 0 - self._device_pbtype = 'none' - self._device_lambdaueb = 'UP' - self._old_pbtype = 'none' - self._mydevicepb = 'none' # type: Any - self._oldrelais = 2 - self._oldwatt = 0 - self._device_chan = 0 - self._device_updatesec = 0 - # mqtt per - self._whimported_tmp = 0 - self.runningtime = 0 - self.oncountnor = '0' - self.oncntstandby = '0' - self._wh = 0 - self._wpos = 0 - self._deviceconfigured = '1' - self._deviceconfiguredold = '9' - # not none ! - self._oldmeasuretype1 = 'empty' - self.c_oldstampeinschaltdauer = 0 - self.c_oldstampeinschaltdauer_f = 'N' - self.c_mantime = 0 - self.c_mantime_f = 'N' - self._c_eintime = 0 - self._c_eintime_f = 'N' - self._c_anlaufz = 0 - self._c_anlaufz_f = 'N' - self._c_ausverz = 0 - self._c_ausverz_f = 'N' - self._c_einverz = 0 - self._c_einverz_f = 'N' - self._c_updatetime = 0 - self._seclastup = 0 - self._dynregel = 0 - self.device_setauto = 0 - self.gruppe = 'none' - self.btchange = 0 - self._mydevicemeasure = 'none' # type: Any def prewatt(self, uberschuss: int, uberschussoffset: int) -> None: self._uberschuss = uberschuss @@ -201,20 +101,53 @@ def postwatt(self) -> None: with open(self._basePath+'/ramdisk/smarthome_device_' + str(self.device_nummer) + 'watt0pos', 'r') as value: importtemp = int(value.read()) - self.simcount(self._oldwatt, "smarthome_device_" + - str(self.device_nummer), - "device" + str(self.device_nummer) + "_wh", - "device" + str(self.device_nummer) + "_whe", - str(self.device_nummer), self.newwattk) + if (self.newwattk > 0): + # Shadow calculation for devices mit gelierten Zaehler (z.b. sdm630) + self.newwattks = self.simcount(self._oldwatt, "smarthome_device_" + + str(self.device_nummer), + "device" + str(self.device_nummer) + "_wh", + "device" + str(self.device_nummer) + "_whe", + str(self.device_nummer), self.newwattk) + # str(self.device_nummer), 0) + # um Simulation zweiter Zaehler zu aktivieren + # + else: + # uebernehmen gerechneten Zaehlerstand für alle anderen devices (z.b. shelly) + self.newwattk = self.simcount(self._oldwatt, "smarthome_device_" + + str(self.device_nummer), + "device" + str(self.device_nummer) + "_wh", + "device" + str(self.device_nummer) + "_whe", + str(self.device_nummer), 0) + except Exception: + # first run simcount also update + # add start point for shadow importtemp = self._whimported_tmp - with open(self._basePath+'/ramdisk/smarthome_device_' + str(self.device_nummer) + 'watt0pos', 'w') as f: f.write(str(importtemp)) with open(self._basePath+'/ramdisk/smarthome_device_' + str(self.device_nummer) + 'watt0neg', 'w') as f: f.write(str("0")) + if (self.newwattk > 0): + log.info("(" + str(self.device_nummer) + + ") Simcount Startwert aus Z1 (HW) übernommen " + + str(self.newwattk) + " kwh " + str(self.newwattk * 3600) + " wh") + self.newwattks = self.simcount(self._oldwatt, + "smarthome_device_" + + str(self.device_nummer), + "device" + str(self.device_nummer) + "_wh", + "device" + str(self.device_nummer) + "_whe", + str(self.device_nummer), self.newwattk) + else: + log.info("(" + str(self.device_nummer) + + ") Simcount Startwert aus mqtt übernommen " + + str(self._whimported_tmp) + " wh") + self.newwattk = int(self.simcount(self._oldwatt, "smarthome_device_" + + str(self.device_nummer), + "device" + str(self.device_nummer) + "_wh", + "device" + str(self.device_nummer) + "_whe", + str(self.device_nummer), 0)) if (self.relais == 1): newtime = int(time.time()) if (self.c_oldstampeinschaltdauer_f == 'Y'): @@ -237,11 +170,21 @@ def postwatt(self) -> None: self._c_eintime = 0 self._c_eintime_f = 'N' self._oldrelais = self.relais - if (self.device_temperatur_configured > 0): + if (self.device_temperatur_configured == 0): + self.mqtt_param[pref + 'TemperatureSensor0'] = '300' + self.mqtt_param[pref + 'TemperatureSensor1'] = '300' + self.mqtt_param[pref + 'TemperatureSensor2'] = '300' + elif (self.device_temperatur_configured == 1): + self.mqtt_param[pref + 'TemperatureSensor0'] = self.temp0 + self.mqtt_param[pref + 'TemperatureSensor1'] = '300' + self.mqtt_param[pref + 'TemperatureSensor2'] = '300' + elif (self.device_temperatur_configured == 2): + self.mqtt_param[pref + 'TemperatureSensor0'] = self.temp0 + self.mqtt_param[pref + 'TemperatureSensor1'] = self.temp1 + self.mqtt_param[pref + 'TemperatureSensor2'] = '300' + else: self.mqtt_param[pref + 'TemperatureSensor0'] = self.temp0 - if (self.device_temperatur_configured > 1): self.mqtt_param[pref + 'TemperatureSensor1'] = self.temp1 - if (self.device_temperatur_configured > 2): self.mqtt_param[pref + 'TemperatureSensor2'] = self.temp2 self.mqtt_param[pref + 'Watt'] = str(self._oldwatt) self.mqtt_param[pref + 'Wh'] = str(self._wh) @@ -457,6 +400,8 @@ def updatepar(self, input_param: Dict[str, str]) -> None: self._mydevicemeasure = Slsdm630() elif (self._device_measuretype == 'lovato'): self._mydevicemeasure = Sllovato() + elif (self._device_measuretype == 'b23'): + self._mydevicemeasure = Slb23() elif (self._device_measuretype == 'sdm120'): self._mydevicemeasure = Slsdm120() elif (self._device_measuretype == 'we514'): @@ -541,18 +486,6 @@ def conditions(self, speichersoc: int) -> None: if ((self.device_canswitch == 0) or (self.device_manual == 1)): return - file_charge = '/var/www/html/openWB/ramdisk/llkombiniert' - testcharge = 0.0 - try: - if os.path.isfile(file_charge): - with open(file_charge, 'r') as f: - testcharge = float(f.read()) - except Exception: - pass - if testcharge <= 1000: - chargestatus = 0 - else: - chargestatus = 1 work_ausschaltschwelle = self._device_ausschaltschwelle work_ausschaltverzoegerung = self._device_ausschaltverzoegerung local_time = datetime.now(timezone.utc).astimezone() @@ -777,8 +710,8 @@ def conditions(self, speichersoc: int) -> None: log.info("(" + str(self.device_nummer) + ") " + self.device_name + " Soll reduziert/abgeschaltet werden" + - " bei Ladung, pruefe " + str(testcharge)) - if chargestatus == 1: + " bei Ladung, pruefe " + str(self.chargestatus)) + if self.chargestatus: log.info("(" + str(self.device_nummer) + ") " + self.device_name + " Ladung läuft, pruefe Mindestlaufzeit") @@ -850,8 +783,8 @@ def conditions(self, speichersoc: int) -> None: log.info("(" + str(self.device_nummer) + ") " + self.device_name + " Soll nicht eingeschaltet werden bei" + - " Ladung, pruefe " + str(testcharge)) - if chargestatus == 1: + " Ladung, pruefe " + str(self.chargestatus)) + if self.chargestatus: log.info("(" + str(self.device_nummer) + ") " + self.device_name + " Ladung läuft, " + "wird nicht eingeschaltet") @@ -1138,7 +1071,14 @@ def conditions(self, speichersoc: int) -> None: self._c_einverz_f = 'N' self._c_ausverz_f = 'N' - def simcount(self, watt2: int, pref: str, importfn: str, exportfn: str, nummer: str, wattks: int) -> None: + def simcount(self, watt2: int, pref: str, importfn: str, exportfn: str, nummer: str, wattks: int) -> int: + # if (nummer == "1"): + # debug = True + # else: + # debug = False + seconds2 = time.time() + watt1 = 0 + seconds1 = 0.0 # Zaehler mitgeliefert in WH , zurueckrechnen fuer simcount if wattks > 0: wattposkh = wattks @@ -1150,16 +1090,19 @@ def simcount(self, watt2: int, pref: str, importfn: str, exportfn: str, nummer: self._wpos = wattposh with open(self._basePath+'/ramdisk/'+pref+'watt0neg', 'w') as f: f.write(str(wattnegh)) - self._wh = round(wattposkh, 2) with open(self._basePath+'/ramdisk/' + importfn, 'w') as f: f.write(str(round(wattposkh, 2))) with open(self._basePath+'/ramdisk/' + exportfn, 'w') as f: f.write(str(wattnegkh)) - return + # start punkt für simulation schreiben + value1 = "%22.6f" % seconds2 + with open(self._basePath+'/ramdisk/'+pref+'sec0', 'w') as f: + f.write(str(value1)) + with open(self._basePath+'/ramdisk/'+pref+'wh0', 'w') as f: + f.write(str(watt2)) + self._wh = round(wattposkh, 2) + return self._wh # emulate import export - seconds2 = time.time() - watt1 = 0 - seconds1 = 0.0 if os.path.isfile(self._basePath+'/ramdisk/'+pref+'sec0'): with open(self._basePath+'/ramdisk/'+pref+'sec0', 'r') as f: seconds1 = float(f.read()) @@ -1169,42 +1112,49 @@ def simcount(self, watt2: int, pref: str, importfn: str, exportfn: str, nummer: wattposh = int(f.read()) with open(self._basePath+'/ramdisk/'+pref+'watt0neg', 'r') as f: wattnegh = int(f.read()) - value1 = "%22.6f" % seconds2 - with open(self._basePath+'/ramdisk/'+pref+'sec0', 'w') as f: - f.write(str(value1)) with open(self._basePath+'/ramdisk/'+pref+'wh0', 'w') as f: f.write(str(watt2)) seconds1 = seconds1 + 1 deltasec = seconds2 - seconds1 - deltasectrun = int(deltasec * 1000) / 1000 - stepsize = int((watt2-watt1)/deltasec) - while seconds1 <= seconds2: + stepsize = int((watt2-watt1)/(deltasec + 1)) + # if debug: + # log.info("(" + str(nummer) + + # ")D star wh " + str(wattposh) + + # " kwh " + str(int(wattposh/3600)) + + # " seconds1 " + str(seconds1) + + # " watt1 " + str(watt1) + + # " seconds2 " + str(seconds2) + + # " deltasec " + str(deltasec) + + # " stepsize " + str(stepsize) + + # " watt2 " + str(watt2)) + while seconds1 < seconds2: if watt1 < 0: wattnegh = wattnegh + watt1 else: wattposh = wattposh + watt1 + # if debug: + # log.info("(" + str(nummer) + + # ")D calc wh " + str(wattposh) + + # " kwh " + str(int(wattposh/3600)) + + # " seconds1 " + str(seconds1) + + # " watt1 " + str(watt1)) watt1 = watt1 + stepsize - if stepsize < 0: + if stepsize <= 0: watt1 = max(watt1, watt2) else: watt1 = min(watt1, watt2) seconds1 = seconds1 + 1 - rest = deltasec - deltasectrun - seconds1 = seconds1 - 1 + rest - if rest > 0: - watt1 = int(watt1 * rest) - if watt1 < 0: - wattnegh = wattnegh + watt1 - else: - wattposh = wattposh + watt1 - wattposkh = int(wattposh/3600) + seconds1 = seconds1 - 1 + value1 = "%22.6f" % seconds1 + with open(self._basePath+'/ramdisk/'+pref+'sec0', 'w') as f: + f.write(str(value1)) wattnegkh = int((wattnegh*-1)/3600) with open(self._basePath+'/ramdisk/'+pref+'watt0pos', 'w') as f: f.write(str(wattposh)) self._wpos = wattposh with open(self._basePath+'/ramdisk/'+pref+'watt0neg', 'w') as f: f.write(str(wattnegh)) - self._wh = round(wattposkh, 2) + wattposkh = int(wattposh/3600) with open(self._basePath+'/ramdisk/' + importfn, 'w') as f: f.write(str(round(wattposkh, 2))) with open(self._basePath+'/ramdisk/' + exportfn, 'w') as f: @@ -1215,6 +1165,11 @@ def simcount(self, watt2: int, pref: str, importfn: str, exportfn: str, nummer: f.write(str(value1)) with open(self._basePath+'/ramdisk/'+pref+'wh0', 'w') as f: f.write(str(watt2)) + with open(self._basePath+'/ramdisk/'+pref+'watt0pos', 'r') as f: + wattposh = int(f.read()) + wattposkh = int(wattposh/3600) + self._wh = round(wattposkh, 2) + return self._wh def getwatt(self, uberschuss: int, uberschussoffset: int) -> None: self.prewatt(uberschuss, uberschussoffset) diff --git a/packages/smarthome/smartbase0.py b/packages/smarthome/smartbase0.py index 0a19a6127..bb9a52678 100644 --- a/packages/smarthome/smartbase0.py +++ b/packages/smarthome/smartbase0.py @@ -3,6 +3,7 @@ import os import subprocess import logging +from typing import Any, Dict from typing import List log = logging.getLogger(__name__) @@ -11,13 +12,120 @@ class Sbase0: _basePath = '/var/www/html/openWB' _prefixpy = _basePath+'/packages/modules/smarthome/' - def readret(self): + def readret(self) -> Dict[str, Any]: with open(self._basePath+'/ramdisk/smarthome_device_ret' + str(self.device_nummer), 'r') as f1: answer = json.loads(json.load(f1)) return answer - def checkbefsend(self): + def __init__(self) -> None: + # setting + super().__init__() + self.mqtt_param = {} # type: Dict[str, str] + self.mqtt_param_del = {} # type: Dict[str, str] + self.device_name = 'none' + self.devstatus = 10 + # (10 = ueberschuss gesteuert oder manual, + # 20 = Anlauferkennung aktiv + # (ausschalten wenn Leistungsaufnahme > Schwelle) + # 30 = gestartet um fertig bis zu erreichen + # default 10 + self._first_run = 1 + self.chargestatus = False + self.device_nummer = 0 + self.temp0 = '300' + self.temp1 = '300' + self.temp2 = '300' + self.newwatt = 0 + self.newwattk = 0 + self.newwattks = 0 + self.pvwatt = 0 + self.relais = 0 + self.devuberschuss = 0 + self.device_temperatur_configured = 0 + self.ueberschussberechnung = 1 + self.abschalt = 0 + self.device_homeconsumtion = 0 + self.device_manual = 0 + self.device_manual_control = 0 + self.newdevice_manual = 0 + self.newdevice_manual_control = 0 + self.device_type = 'none' + self._smart_param = {} # type: Dict[str, str] + self._uberschussoffset = 0 + self._uberschuss = 0 + self.device_canswitch = 0 + self._device_deactivatewhileevcharging = 0 + self._device_mineinschaltdauer = 0 + self._device_mindayeinschaltdauer = 0 + self._device_maxeinschaltdauer = 0 + self._device_differentmeasurement = 0 + self._device_speichersocbeforestop = 100 + self._device_speichersocbeforestart = 0 + self._device_startupdetection = 0 + self._device_standbypower = 0 + self._device_standbyduration = 0 + self._device_startupmuldetection = 0 + self._device_einschaltschwelle = 0 + self._device_ausschaltschwelle = 0 + self._device_einschaltverzoegerung = 0 + self._device_ausschaltverzoegerung = 0 + self._device_configured = '0' + self._device_ip = 'none' + self._device_measuretype = 'none' + self._device_measureip = 'none' + self._device_measureportsdm = 8899 + self._device_dacport = 8899 + self._device_measureid = 0 + self._device_finishtime = '00:00' + self._device_starttime = '00:00' + self._device_endtime = '00:00' + self._device_ontime = '00:00' + self._device_offtime = '00:00' + self._device_onuntiltime = '00:00' + self._device_nonewatt = 0 + self._device_deactivateper = 0 + self._device_pbtype = 'none' + self._device_lambdaueb = 'UP' + self._old_pbtype = 'none' + self._mydevicepb = 'none' # type: Any + self._oldrelais = 2 + self._oldwatt = 0 + self._device_chan = 0 + self._device_updatesec = 0 + # mqtt per + self._whimported_tmp = 0 + self.runningtime = 0 + self.oncountnor = '0' + self.oncntstandby = '0' + self._wh = 0 + self._wpos = 0 + self._deviceconfigured = '1' + self._deviceconfiguredold = '9' + # not none ! + self._oldmeasuretype1 = 'empty' + self.c_oldstampeinschaltdauer = 0 + self.c_oldstampeinschaltdauer_f = 'N' + self.c_mantime = 0 + self.c_mantime_f = 'N' + self._c_eintime = 0 + self._c_eintime_f = 'N' + self._c_anlaufz = 0 + self._c_anlaufz_f = 'N' + self._c_ausverz = 0 + self._c_ausverz_f = 'N' + self._c_einverz = 0 + self._c_einverz_f = 'N' + self._c_updatetime = 0 + self._seclastup = 0 + self._dynregel = 0 + self.device_setauto = 0 + self.gruppe = 'none' + self.btchange = 0 + self._mydevicemeasure = 'none' # type: Any + self.device_nummer = 0 + + def checkbefsend(self) -> int: newtime = int(time.time()) if (self._c_updatetime == 0): self._c_updatetime = newtime - 180 @@ -34,7 +142,7 @@ def checkbefsend(self): forcesend = 9 return forcesend - def checksend(self, answer): + def checksend(self, answer: Any) -> None: try: send = int(answer['send']) sendpower = int(answer['sendpower']) diff --git a/packages/smarthome/smartcommon.py b/packages/smarthome/smartcommon.py index 1eacf598b..602f1348a 100644 --- a/packages/smarthome/smartcommon.py +++ b/packages/smarthome/smartcommon.py @@ -110,7 +110,7 @@ def on_message(client, userdata, msg) -> None: log.warning(" Skipped msg " + msg.topic + " Value " + value) -def getdevicevalues(uberschuss: int, uberschussoffset: int, pvwatt: int) -> None: +def getdevicevalues(uberschuss: int, uberschussoffset: int, pvwatt: int, chargestatus: bool) -> None: global mydevices totalwatt = 0 totalwattot = 0 @@ -122,9 +122,11 @@ def getdevicevalues(uberschuss: int, uberschussoffset: int, pvwatt: int) -> None mqtt_all = {} for mydevice in mydevices: mydevice.pvwatt = pvwatt + mydevice.chargestatus = chargestatus mydevice.getwatt(uberschuss, uberschussoffset) watt = mydevice.newwatt wattk = mydevice.newwattk + wattks = mydevice.newwattks relais = mydevice.relais # temp0 = mydevice.temp0 # temp1 = mydevice.temp1 @@ -143,7 +145,7 @@ def getdevicevalues(uberschuss: int, uberschussoffset: int, pvwatt: int) -> None str(mydevice.runningtime) + " Status/Üeb: " + str(mydevice.devstatus) + "/" + str(mydevice.ueberschussberechnung) + " akt: " + str(watt) + - " Z: " + str(wattk)) + " Z1: " + str(wattk) + " Z2: " + str(wattks)) # mqtt_all.update(mydevice.mqtt_param) for keyread, value in mydevice.mqtt_param.items(): key = mqttsdevstat + keyread @@ -390,7 +392,8 @@ def resetmaxeinschaltdauerfunc() -> None: resetmaxeinschaltdauer = 0 -def loadregelvars(wattbezug: int, speicherleistung: int, speichersoc: int, pvwatt: int) -> Tuple[int, int]: +def loadregelvars(wattbezug: int, speicherleistung: int, speichersoc: int, + pvwatt: int, chargestatus: bool) -> Tuple[int, int]: global maxspeicher global mydevices uberschuss = wattbezug + speicherleistung @@ -400,7 +403,7 @@ def loadregelvars(wattbezug: int, speicherleistung: int, speichersoc: int, pvwat log.info("Uberschuss: " + str(uberschuss) + " Uberschuss mit Offset: " + str(uberschussoffset) + " Pv: " + str(pvwatt)) log.info("Speicher Entladung(-)/Ladung(+): " + - str(speicherleistung) + " SpeicherSoC: " + str(speichersoc)) + str(speicherleistung) + " SpeicherSoC: " + str(speichersoc) + " Ladung: " + str(chargestatus)) reread = 0 try: with open(bp+'/ramdisk/rereadsmarthomedevices', 'r') as value: @@ -449,16 +452,17 @@ def initparam(inpcg: str, inpcs: str, inpsdevstat: str, inpsglobstat: str, inpto mqttport = inpport -def mainloop(wattbezug: int, speicherleistung: int, speichersoc: int, pvwatt: int = 0) -> None: +def mainloop(wattbezug: int, speicherleistung: int, speichersoc: int, pvwatt: int = 0, + chargestatus: bool = False) -> None: global firststart if firststart: readmq() firststart = False mqtt_man = {} sendmess = 0 - uberschuss, uberschussoffset = loadregelvars(wattbezug, speicherleistung, speichersoc, pvwatt) + uberschuss, uberschussoffset = loadregelvars(wattbezug, speicherleistung, speichersoc, pvwatt, chargestatus) resetmaxeinschaltdauerfunc() - getdevicevalues(uberschuss, uberschussoffset, pvwatt) + getdevicevalues(uberschuss, uberschussoffset, pvwatt, chargestatus) conditions(speichersoc) # do the manual stuff for i in range(1, (numberOfSupportedDevices+1)): diff --git a/packages/smarthome/smartmeas.py b/packages/smarthome/smartmeas.py index 444d06e8a..da571ea2c 100644 --- a/packages/smarthome/smartmeas.py +++ b/packages/smarthome/smartmeas.py @@ -1,7 +1,7 @@ from smarthome.smartbase0 import Sbase0 from typing import Dict, Tuple from modules.common import modbus -from modules.common import sdm +from modules.common import sdm, b23 from modules.common import lovato import logging log = logging.getLogger(__name__) @@ -461,6 +461,27 @@ def sepwattread(self) -> Tuple[int, int]: return self.newwatt, self.newwattk +class Slb23(Slbase): + def __init__(self) -> None: + # setting + super().__init__() + + def sepwattread(self) -> Tuple[int, int]: + try: + # neu aus openwb 2.0 + with modbus.ModbusTcpClient_(self._device_measureip, self._device_measureportsdm) as tcp_client: + b23inst = b23.B23(self._device_measureid, tcp_client) + # log.warning(" b23inst id %s " % ( str(id(b23inst)))) + _, newwatt = b23inst.get_power() + self.newwatt = int(newwatt) + self.newwattk = int(b23inst.get_imported()) + except Exception as e1: + log.warning("Leistungsmessung %s %d %s Fehlermeldung: %s " + % ('b23 ', self.device_nummer, + str(self._device_measureip), str(e1))) + return self.newwatt, self.newwattk + + class Slsdm630(Slbase): def __init__(self) -> None: # setting diff --git a/runs/atreboot.sh b/runs/atreboot.sh index 9c9edca5f..86b645d58 100755 --- a/runs/atreboot.sh +++ b/runs/atreboot.sh @@ -32,6 +32,14 @@ at_reboot() { sudo kill "$$" ) & + # check for outdated sources.list (Stretch only) + if grep -q -e "^deb http://raspbian.raspberrypi.org/raspbian/ stretch" /etc/apt/sources.list; then + echo "sources.list outdated! upgrading..." + sudo sed -i "s/^deb http:\/\/raspbian.raspberrypi.org\/raspbian\/ stretch/deb http:\/\/legacy.raspbian.org\/raspbian\/ stretch/g" /etc/apt/sources.list + else + echo "sources.list already updated" + fi + # read openwb.conf echo "loading config" . "$OPENWBBASEDIR/loadconfig.sh" @@ -286,20 +294,6 @@ at_reboot() { ln -s "$VWIDMODULEDIR/_secrets.py" "$VWIDMODULEDIR/secrets.py" fi fi - #Prepare for secrets used in soc module soc_smarteq in Python - SMARTEQMODULEDIR="$OPENWBBASEDIR/modules/soc_smarteq" - if python3 -c "import secrets" &>/dev/null; then - echo 'soc_smarteq: python3 secrets installed...' - if [ -L "$SMARTEQMODULEDIR/secrets.py" ]; then - echo 'soc_smarteq: remove local python3 secrets.py...' - rm "$SMARTEQMODULEDIR/secrets.py" - fi - else - if [ ! -L "$SMARTEQMODULEDIR/secrets.py" ]; then - echo 'soc_smarteq: enable local python3 secrets.py...' - ln -s "$SMARTEQMODULEDIR/_secrets.py" "$SMARTEQMODULEDIR/secrets.py" - fi - fi # update outdated urllib3 for Tesla Powerwall pip3 install --upgrade urllib3 diff --git a/runs/isss.py b/runs/isss.py index e56cb4d0c..1728307ce 100755 --- a/runs/isss.py +++ b/runs/isss.py @@ -76,6 +76,11 @@ def update_values(self, counter_state: ChargepointState) -> None: self._pub_values_to_1_9(key, value) self._pub_values_to_2(key, value) self.old_counter_state = counter_state + for topic, value in [ + ("fault_state", 0), + ("fault_str", "Keine Fehler.") + ]: + self._pub_values_to_2(topic, value) def _pub_values_to_1_9(self, key: str, value) -> None: def pub_value(topic: str, value): @@ -84,7 +89,7 @@ def pub_value(topic: str, value): if self.parent_wb != "localhost": pub_single("openWB/lp/"+self.cp_num_str+"/"+topic, payload=str(value), hostname=self.parent_wb, no_json=True) - topic = self.MAP_KEY_TO_OLD_TOPIC[key] + topic = self.MAP_KEY_TO_OLD_TOPIC.get(key) rounding = get_rounding_function_by_digits(2) if topic is not None: if isinstance(topic, List): diff --git a/runs/mqttsub.py b/runs/mqttsub.py index d44865ea9..fe931cbd4 100755 --- a/runs/mqttsub.py +++ b/runs/mqttsub.py @@ -144,7 +144,7 @@ def map_run(message: str, device_number: int, option: str): "device_shauth": create_topic_handler(int_range_validator(0, 1)), "device_measureshauth": create_topic_handler(int_range_validator(0, 1)), "device_chan": create_topic_handler(int_range_validator(0, 6)), - "device_nxdacxxtype": create_topic_handler(int_range_validator(0, 2)), + "device_nxdacxxtype": create_topic_handler(int_range_validator(0, 3)), "device_measchan": create_topic_handler(int_range_validator(0, 6)), "device_ip": create_topic_handler(ip_address_validator), "device_pbip": create_topic_handler(ip_address_validator), @@ -156,7 +156,7 @@ def map_run(message: str, device_number: int, option: str): "stiebel", "http", "avm", "mystrom", "viessmann", "mqtt", "NXDACXX", "ratiotherm" )), "device_measureType": create_topic_handler( - equals_one_of_validator("shelly", "tasmota", "http", "mystrom", "sdm630", "lovato", "we514", "fronius", + equals_one_of_validator("shelly", "tasmota", "http", "mystrom", "sdm630", "lovato", "we514", "fronius", "b23", "json", "avm", "mqtt", "sdm120", "smaem")), "device_temperatur_configured": create_topic_handler(int_range_validator(0, 3)), "device_einschaltschwelle": create_topic_handler(int_range_validator(-100000, 100000)), @@ -190,15 +190,17 @@ def map_run(message: str, device_number: int, option: str): "device_shusername": create_topic_handler(), "device_shpassword": create_topic_handler(), "device_manwatt": create_topic_handler(int_range_validator(0, 30000)), + "device_maxueb": create_topic_handler(int_range_validator(0, 30000)), "device_measureshusername": create_topic_handler(), "device_measureshpassword": create_topic_handler(), "device_actor": create_topic_handler(), "device_measureavmusername": create_topic_handler(), "device_measureavmpassword": create_topic_handler(), "device_measureavmactor": create_topic_handler(), - "device_acthortype": create_topic_handler(equals_one_of_validator("M1", "M3", "9s", "9s18")), + "device_acthortype": create_topic_handler(equals_one_of_validator("M1", "M3", "9s", "9s18", "9s27", "9s45", "E2M1", "E2M3")), "device_lambdaueb": create_topic_handler(equals_one_of_validator("UP", "UN", "UZ")), - "device_acthorpower": create_topic_handler(int_range_validator(0, 18000)), + "device_idmueb": create_topic_handler(equals_one_of_validator("UP", "UZ")), + "device_acthorpower": create_topic_handler(int_range_validator(0, 50000)), "device_finishTime": create_topic_handler(regex_match_validator(r"^([01]{0,1}\d|2[0-3]):[0-5]\d$")), "device_onTime": create_topic_handler(regex_match_validator(r"^([01]{0,1}\d|2[0-3]):[0-5]\d$")), "device_offTime": create_topic_handler(regex_match_validator(r"^([01]{0,1}\d|2[0-3]):[0-5]\d$")), diff --git a/runs/smarthomemq.py b/runs/smarthomemq.py index 63821d1dd..29a70267e 100755 --- a/runs/smarthomemq.py +++ b/runs/smarthomemq.py @@ -2,6 +2,7 @@ from smarthome.smartcommon import mainloop, initparam import time import logging +import os log = logging.getLogger("smarthome") # openwb 1.9 spec mqttcg = 'openWB/config/get/SmartHome/' @@ -99,5 +100,24 @@ def checkbootdone() -> int: log.warning("Fehler beim Auslesen der Ramdisk (wattbezug):" + str(e)) wattbezug = 0 - mainloop(wattbezug, speicherleistung, speichersoc) + try: + with open(bp+'/ramdisk/pvallwatt', 'r') as value: + pvwatt = int(float(value.read())) * -1 + except Exception as e: + log.warning("Fehler beim Auslesen der Ramdisk (pvallwatt):" + + str(e)) + pvwatt = 0 + file_charge = '/var/www/html/openWB/ramdisk/llkombiniert' + testcharge = 0.0 + try: + if os.path.isfile(file_charge): + with open(file_charge, 'r') as f: + testcharge = float(f.read()) + except Exception: + pass + if testcharge <= 1000: + chargestatus = False + else: + chargestatus = True + mainloop(wattbezug, speicherleistung, speichersoc, pvwatt, chargestatus) time.sleep(5) diff --git a/runs/updateConfig.sh b/runs/updateConfig.sh index b2033f19d..a4c88c1be 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 @@ -581,11 +587,11 @@ updateConfig(){ if ! grep -Fq "soc_id_intervall=" $ConfigFile; then echo "soc_id_intervall=120" >> $ConfigFile fi - if ! grep -Fq "soc_smarteq_intervallladen=" $ConfigFile; then - echo "soc_smarteq_intervallladen=20" >> $ConfigFile + if ! grep -Fq "soc_ovms_intervallladen=" $ConfigFile; then + echo "soc_ovms_intervallladen=20" >> $ConfigFile fi - if ! grep -Fq "soc_smarteq_intervall=" $ConfigFile; then - echo "soc_smarteq_intervall=120" >> $ConfigFile + if ! grep -Fq "soc_ovms_intervall=" $ConfigFile; then + echo "soc_ovms_intervall=120" >> $ConfigFile fi if ! grep -Fq "releasetrain=" $ConfigFile; then echo "releasetrain=stable" >> $ConfigFile @@ -2106,21 +2112,36 @@ updateConfig(){ if ! grep -Fq "soc_id_vin=" $ConfigFile; then echo "soc_id_vin=VIN" >> $ConfigFile fi - if ! grep -Fq "soc_smarteq_username=" $ConfigFile; then - echo "soc_smarteq_username=User" >> $ConfigFile + if ! grep -Fq "soc_ovms_server=" $ConfigFile; then + echo "soc_ovms_server=https://ovms.dexters-web.de:6869" >> $ConfigFile + fi + if ! grep -Fq "soc2server=" $ConfigFile; then + echo "soc2server=https://ovms.dexters-web.de:6869" >> $ConfigFile fi - if ! grep -Fq "soc_smarteq_passwort=" $ConfigFile; then - echo "soc_smarteq_passwort=''" >> $ConfigFile + if ! grep -Fq "soc_ovms_username=" $ConfigFile; then + echo "soc_ovms_username=User" >> $ConfigFile + fi + if ! grep -Fq "soc_ovms_passwort=" $ConfigFile; then + echo "soc_ovms_passwort=''" >> $ConfigFile else - sed -i "/soc_smarteq_passwort='/b; s/^soc_smarteq_passwort=\(.*\)/soc_smarteq_passwort=\'\1\'/g" $ConfigFile + sed -i "/soc_ovms_passwort='/b; s/^soc_ovms_passwort=\(.*\)/soc_ovms_passwort=\'\1\'/g" $ConfigFile fi - if ! grep -Fq "soc_smarteq_vin=" $ConfigFile; then - echo "soc_smarteq_vin=VIN" >> $ConfigFile + if ! grep -Fq "soc_ovms_vehicleid=" $ConfigFile; then + echo "soc_ovms_vehicleid=vehicleid" >> $ConfigFile fi if ! grep -Fq "soc2vin=" $ConfigFile; then echo "soc2vin=" >> $ConfigFile echo "soc2intervall=60" >> $ConfigFile fi + if ! grep -Fq "soc2vehicleid=" $ConfigFile; then + echo "soc2vehicleid=" >> $ConfigFile + fi + if ! grep -Fq "soc2intervall=" $ConfigFile; then + echo "soc2intervall=60" >> $ConfigFile + fi + if ! grep -Fq "soc2intervallladen=" $ConfigFile; then + echo "soc2intervallladen=10" >> $ConfigFile + fi if ! grep -Fq "wirkungsgradlp1=" $ConfigFile; then echo "wirkungsgradlp1=90" >> $ConfigFile fi @@ -2358,5 +2379,32 @@ updateConfig(){ fi fi fi + echo "remove soc_smarteq entries from Config file" + cp $ConfigFile $ConfigFile.tmp + # check for socmodul1=soc_smarteqlp2 + ism2=`grep "socmodul1=soc_smarteqlp2" $ConfigFile.tmp | wc -l | awk '{print $1}'` + if [ $ism2 -ne 0 ] + then + echo "soc_smarteq found configured as socmodul1 - cleanup soc2 entries" + sed -e " + s/soc2user=.*$/soc2user=demo@demo.de/ + s/soc2pass=.*$/soc2pass=\'\'/ + s/soc2pin=.*$/soc2pin=pin/ + s/soc2vin=.*$/soc2vin=/ + s/soc2intervall=.*$/soc2intervall=60/ + s/soc2intervallladen=.*$/soc2intervallladen=10/ + " $ConfigFile.tmp > $ConfigFile.tmp.out + cp $ConfigFile.tmp.out $ConfigFile.tmp + fi + # modify configured smarteq modules to none + sed -e ' + s/socmodul=soc_smarteq/socmodul=none/ + s/socmodul1=soc_smarteqlp2/socmodul1=none/ + /soc2pint=/d + /soc_smarteq/d + ' $ConfigFile.tmp > $ConfigFile.tmp.out + cp $ConfigFile.tmp.out $ConfigFile + rm $ConfigFile.tmp $ConfigFile.tmp.out + echo "Config file Update done." } diff --git a/web/display/minimal/index.php b/web/display/minimal/index.php index fbd54a75f..5cce40a73 100644 --- a/web/display/minimal/index.php +++ b/web/display/minimal/index.php @@ -45,6 +45,13 @@ list($key, $value) = explode("=", $line, 2); ${$key."old"} = trim( $value, " '\t\n\r\0\x0B" ); // remove all garbage and single quotes } + if ($lastmanagementold == "1") { + // filter local connections + $valid_evsecon = ["modbusevse", "daemon", "ipevse", "dac"]; + if (!in_array($evsecons1old, $valid_evsecon)) { + $lastmanagementold = "0"; + } + } ?> @@ -190,7 +197,7 @@ function checkTime(i) { // functions for processing messages 'display/minimal/processAllMqttMsg.js?ver=20230204', // functions performing mqtt and start mqtt-service - 'display/minimal/setupMqttServices.js?ver=20221229', + 'display/minimal/setupMqttServices.js?ver=20230818', ]; scriptsToLoad.forEach(function(src) { diff --git a/web/display/minimal/setupMqttServices.js b/web/display/minimal/setupMqttServices.js index e4d674f87..395e9b247 100644 --- a/web/display/minimal/setupMqttServices.js +++ b/web/display/minimal/setupMqttServices.js @@ -17,14 +17,19 @@ var topicsToSubscribe = [ ["openWB/lp/1/boolPlugStat", 1], ["openWB/lp/1/boolChargeStat", 1], ["openWB/lp/1/ChargePointEnabled", 1], - ["openWB/lp/2/W", 1], - ["openWB/lp/2/%Soc", 1], - ["openWB/lp/2/boolSocConfigured"], - ["openWB/lp/2/boolPlugStat", 1], - ["openWB/lp/2/ChargePointEnabled", 1], - ["openWB/lp/2/boolChargeStat", 1], ]; +if (lastmanagementold == 1) { + topicsToSubscribe.push( + ["openWB/lp/2/W", 1], + ["openWB/lp/2/%Soc", 1], + ["openWB/lp/2/boolSocConfigured"], + ["openWB/lp/2/boolPlugStat", 1], + ["openWB/lp/2/ChargePointEnabled", 1], + ["openWB/lp/2/boolChargeStat", 1], + ); +} + // holds number of topics flagged 1 initially var countTopicsNotForPreloader = topicsToSubscribe.filter(row => row[1] === 1).length; diff --git a/web/settings/modulconfiglp.php b/web/settings/modulconfiglp.php index 021535665..083fa343f 100644 --- a/web/settings/modulconfiglp.php +++ b/web/settings/modulconfiglp.php @@ -763,7 +763,7 @@ function visibility_twcmanagerlp1_connection() { - + @@ -1295,52 +1295,62 @@ function visibility_kia_advanced() { -
+
- Für smart EQ Fahrzeuge. Es wird benötigt:
- - smart Control Account aktiv
+ Für Fahrzeuge mit OVMS Modul. Es wird benötigt:
+ - OVMS Account in z.B. dexters-web.de
- +
- + - Email Adresse des Logins. + URL des OVMS Servers incl. Port.
+ z.B. https://ovms.dexters-web.de:6869
- +
- + - Password des Logins. + User Name des Accounts in z.B. dexters-web.de.
- +
- + - Vollständige VIN des Fahrzeugs. + Password des Accounts. + +
+
+
+ +
+ + + OVMS vehicleId des Fahrzeugs.
- +
- + Wie oft das Fahrzeug abgefragt wird, wenn nicht geladen wird. Angabe in Minuten.
- +
- + Wie oft das Fahrzeug abgefragt wird, wenn geladen wird. Angabe in Minuten. @@ -1796,6 +1806,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! +
+
+
@@ -2372,7 +2404,7 @@ function display_socmodul() { hideSection('#socmaudi'); hideSection('#socmid'); hideSection('#socmvwid'); - hideSection('#socmsmarteq'); + hideSection('#socmovms'); hideSection('#socvag'); hideSection('#socevcc'); hideSection('#socmqtt'); @@ -2407,10 +2439,10 @@ function display_socmodul() { showSection('#socsupportinfo'); showSection('#socmvwid'); } - if($('#socmodul').val() == 'soc_smarteq') { - $('#socsuportlink').attr('href', 'https://openwb.de/forum/viewtopic.php?f=12&t=6222') + if($('#socmodul').val() == 'soc_ovms') { + $('#socsuportlink').attr('href', 'https://forum.openwb.de/viewtopic.php?t=9278') showSection('#socsupportinfo'); - showSection('#socmsmarteq'); + showSection('#socmovms'); } if($('#socmodul').val() == 'soc_vag') { showSection('#socoldevccwarning'); @@ -3086,7 +3118,7 @@ function visibility_twcmanagerlp2_connection() { - + @@ -3137,9 +3169,9 @@ function visibility_twcmanagerlp2_connection() { - We Connect (ID) Account aktiv
- We Connect ID App eingerichtet - auch für nicht-ID!
-
- Für smart EQ Fahrzeuge. Es wird benötigt:
- - smart Control Account aktiv
+
+ Für Fahrzeuge mit OVMS Modul. Es wird benötigt:
+ - OVMS Account in z.B. dexters-web.de
@@ -3549,6 +3581,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! +
+
+
@@ -3626,7 +3680,35 @@ function visibility_i3_soccalclp2() {
- PIN des Accounts. + PIN des Accounts.
+ Bei Smart EQ kommt die PIN (OTP Code) via Email.
+
+
+
+
+
+
+
+
+ +
+ + + URL des OVMS Servers incl. Port.
+ z.B. https://ovms.dexters-web.de:6869
+
+
+
+
+
+
+
+
+ +
+ + + OVMS vehicleId des Fahrzeugs.
@@ -4378,6 +4460,7 @@ function display_socmodul1() { hideSection('#socmodullp2'); hideSection('#socmqtt1'); hideSection('#socmtype2'); + hideSection('#socmserver2'); hideSection('#socmuser2'); hideSection('#socmpass2'); hideSection('#socmpin2'); @@ -4394,6 +4477,7 @@ function display_socmodul1() { hideSection('#socmzeronglp2'); hideSection('#socpsalp2'); hideSection('#socmvin2'); + hideSection('#socmvehicleid2'); hideSection('#socmintervall2'); hideSection('#socmintervallladen2'); hideSection('#socmanuallp2'); @@ -4402,7 +4486,7 @@ function display_socmodul1() { hideSection('#socmkialp2'); hideSection('#socoldevccwarninglp2'); hideSection('#socmvwidinfolp2'); - hideSection('#socmsmarteqinfolp2'); + hideSection('#socmovmsinfolp2'); hideSection('#socsupportinfolp2'); hideSection('#socnosupportinfolp2'); @@ -4452,13 +4536,14 @@ function display_socmodul1() { showSection('#socmintervall2'); showSection('#socmintervallladen2'); } - if($('#socmodul1').val() == 'soc_smarteqlp2') { - $('#socsuportlinklp2').attr('href', 'https://openwb.de/forum/viewtopic.php?f=12&t=6222') + if($('#socmodul1').val() == 'soc_ovmslp2') { + $('#socsuportlinklp2').attr('href', 'https://forum.openwb.de/viewtopic.php?t=9278') showSection('#socsupportinfolp2'); - showSection('#socmsmarteqinfolp2'); + showSection('#socmovmsinfolp2'); + showSection('#socmserver2'); showSection('#socmuser2'); showSection('#socmpass2'); - showSection('#socmvin2'); + showSection('#socmvehicleid2'); showSection('#socmintervall2'); showSection('#socmintervallladen2'); } diff --git a/web/settings/modulconfigpv.php b/web/settings/modulconfigpv.php index 584af4552..08e369234 100644 --- a/web/settings/modulconfigpv.php +++ b/web/settings/modulconfigpv.php @@ -202,6 +202,13 @@
+
+ +
+ + Hier kann ein abweichender Netzwerk-Port angegeben werden, auf dem die Modbus/TCP Verbindung aufgebaut wird.Standard ist 502. +
+
diff --git a/web/settings/mqttapi.php b/web/settings/mqttapi.php index 438cf1ee7..c480d4c6a 100644 --- a/web/settings/mqttapi.php +++ b/web/settings/mqttapi.php @@ -8,7 +8,7 @@ $message = $_GET["message"]; # check if topic is allowed to write if(strpos($topic, "/set/") !== false){ - $command = "mosquitto_pub -h localhost -t '$topic' -m '$message' 2>&1"; + $command = "mosquitto_pub -h localhost -t " . escapeshellarg($topic) . " -m " . escapeshellarg($message) . " 2>&1"; $output = exec($command); # Skip an annoying warning because it doesn't cause any problems $output = str_replace("Warning: Unable to locate configuration directory, default config not loaded.", "", $output); @@ -29,7 +29,7 @@ } # reading topic else{ - $command = "mosquitto_sub -h localhost -t '$topic' -C 1 -W 1 2>&1"; + $command = "mosquitto_sub -h localhost -t " . escapeshellarg($topic) . " -C 1 -W 1 2>&1"; $output = exec($command); # Skip an annoying warning because it doesn't cause any problems $output = str_replace("Warning: Unable to locate configuration directory, default config not loaded.", "", $output); @@ -48,4 +48,4 @@ http_response_code(400); echo "Error: No 'topic' field provided. \nExample reading a MQTT-Topic: 'http://IP/openWB/web/settings/mqttapi.php?topic=openWB/pv/W' \nExample writing a MQTT-Topic: 'http://IP/openWB/web/settings/mqttapi.php?topic=openWB/set/pv/1/W&message=-1000' "; } -?> \ No newline at end of file +?> diff --git a/web/settings/saveconfig.php b/web/settings/saveconfig.php index ad4fda97c..8d83ba3c2 100644 --- a/web/settings/saveconfig.php +++ b/web/settings/saveconfig.php @@ -66,6 +66,15 @@ // prepare key/value array $settingsArray = []; + function checkModule($module){ + $modulePath = $_SERVER['DOCUMENT_ROOT'] . "/openWB/modules/"; + $path = realpath($modulePath . $module); + if ($path === false || strpos($path, $modulePath) !== 0) { + return false; + } + return basename($path); + } + try { if ( !file_exists($myConfigFile) ) { throw new Exception('Konfigurationsdatei nicht gefunden.'); @@ -98,38 +107,8 @@ } } - // write config to file - $fp = fopen($myConfigFile, "w"); - if ( !$fp ) { - throw new Exception('Konfigurationsdatei konnte nicht geschrieben werden.'); - } - foreach($settingsArray as $key => $value) { - // only save to config if $key has some meaningful length - if( strlen($key) > 0 ){ - fwrite($fp, $key."=".$value."\n"); - } - } - fclose($fp); - // handling of different actions required by some modules - // check for manual ev soc module on lp1 - if( array_key_exists( 'socmodul', $_POST ) ){ - if( $_POST['socmodul'] == 'soc_manual' ){ - exec( 'mosquitto_pub -t openWB/lp/1/boolSocManual -r -m "1"' ); - } else { - exec( 'mosquitto_pub -t openWB/lp/1/boolSocManual -r -m "0"' ); - } - } - // check for manual ev soc module on lp2 - if( array_key_exists( 'socmodul1', $_POST ) ){ - if( $_POST['socmodul1'] == 'soc_manuallp2' ){ - exec( 'mosquitto_pub -t openWB/lp/2/boolSocManual -r -m "1"' ); - } else { - exec( 'mosquitto_pub -t openWB/lp/2/boolSocManual -r -m "0"' ); - } - } - // update display process if in POST data if( array_key_exists( 'displayaktiv', $_POST ) || array_key_exists( 'isss', $_POST) ){ ?> @@ -139,25 +118,61 @@ } // start etprovider update if in POST data - if( array_key_exists( 'etprovideraktiv', $_POST ) && ($_POST['etprovideraktiv'] == 1) ){ ?> + if( array_key_exists( 'etprovideraktiv', $_POST ) && ($_POST['etprovideraktiv'] == 1) ){ + $module = checkModule($_POST['etprovider']); + if( $module === false ){ + throw new Exception('Ungültiger ET-Provider: ' . $_POST['etprovider']); + }?> > /var/log/openWB.log 2>&1 &" ); - exec( 'mosquitto_pub -t openWB/global/ETProvider/modulePath -r -m "' . $_POST['etprovider'] . '"' ); + exec( $_SERVER['DOCUMENT_ROOT'] . "/openWB/modules/" . escapeshellcmd($module) . "/main.sh >> /var/log/openWB.log 2>&1 &" ); + exec( 'mosquitto_pub -t openWB/global/ETProvider/modulePath -r -m ' . $module ); } - // start ev-soc updates if in POST data - if( array_key_exists( 'socmodul', $_POST ) && ($_POST['socmodul'] != 'none') ){ ?> - - /dev/null &" ); + // soc module for lp1 + if( array_key_exists( 'socmodul', $_POST ) ){ + // check for manual soc module on lp1 + if( $_POST['socmodul'] == 'soc_manual' ){ + exec( 'mosquitto_pub -t openWB/lp/1/boolSocManual -r -m "1"' ); + } else { + exec( 'mosquitto_pub -t openWB/lp/1/boolSocManual -r -m "0"' ); + } + // start soc update if in POST data + if( $_POST['socmodul'] != 'none' ){ + $module = checkModule($_POST['socmodul']); + if( $module === false ){ + throw new Exception('Ungültiges SoC-Modul: ' . $_POST['socmodul']); + } + ?> + + /dev/null &" ); + } } - if( array_key_exists( 'socmodul1', $_POST ) && ($_POST['socmodul1'] != 'none') ){ ?> - - /dev/null &" ); + + // soc module for lp2 + if( array_key_exists( 'socmodul1', $_POST ) ){ + // check for manual ev soc module on lp2 + if( $_POST['socmodul1'] == 'soc_manuallp2' ){ + exec( 'mosquitto_pub -t openWB/lp/2/boolSocManual -r -m "1"' ); + } else { + exec( 'mosquitto_pub -t openWB/lp/2/boolSocManual -r -m "0"' ); + } + if( $_POST['socmodul1'] != 'none' ){ + // detect trailing "s1" or "lp2" in $POST['socmodul1'] + $moduleName = preg_replace('/(s1|lp2)$/', '', $_POST['socmodul1']); + + $module = checkModule($moduleName); + if( $module === false ){ + throw new Exception('Ungültiges SoC-Modul: ' . $_POST['socmodul1']); + } + ?> + + /dev/null &" ); + } } // check for rfid mode and start/stop handler @@ -167,6 +182,19 @@ exec( $_SERVER['DOCUMENT_ROOT'] . "/openWB/runs/rfid/rfidSetup.sh >> /var/log/openWB.log 2>&1 &" ); } + // write config to file + $fp = fopen($myConfigFile, "w"); + if ( !$fp ) { + throw new Exception('Konfigurationsdatei konnte nicht geschrieben werden.'); + } + foreach($settingsArray as $key => $value) { + // only save to config if $key has some meaningful length + if( strlen($key) > 0 ){ + fwrite($fp, $key."=".$value."\n"); + } + } + fclose($fp); + } catch ( Exception $e ) { $msg = $e->getMessage(); echo ""; diff --git a/web/settings/smarthomeconfig.php b/web/settings/smarthomeconfig.php index 56f51a07a..cfcf156d8 100644 --- a/web/settings/smarthomeconfig.php +++ b/web/settings/smarthomeconfig.php @@ -81,7 +81,7 @@ - + @@ -107,6 +107,7 @@ Wenn die Ausschaltbedingung erreicht ist wird einmalig 0 als Überschuss übertragen. Die Ausschaltschwelle/ Ausschaltverzögerung in OpenWB ist sinnvoll zu wählen (z.B. 500 / 3) um die Regelung von Acthor nicht zu stören. Wenn Acthor als Gerät 1 oder 2 definiert ist, wird die Warmwassertemperatur als Temp1 angezeigt (Modbusadresse 1001). Ebenso wird Temp2 (Modbusadresse 1030) und Temp3 (Modbusadresse 1031) angezeigt (falls angeschlossen). + Elwa2 hat fast die gleich Schnittstelle wie Acthor. Wp der Firma lambda
@@ -282,6 +283,7 @@ + Hier ist das installierte Modell auszuwählen. @@ -377,6 +379,26 @@
+
+
+
+
+ +
+ + + Hier ist die maximale Leistungsaufnahme anzugeben, die idm bei PV Betrieb nicht überschreiten soll. Bei 0 gibt es keine Limitierung bezüglich dem maximal zu übergebenen Überschuss.
+ Sonst wird der zu übergebene Überschuss wie folgt gerechnet: + maximal zu übergeber Überschuss = maximale Leistungsaufnahme - aktuelle Leistungsaufnahme +
+ Sofern die aktuelle Leistungsaufnahme bereits grösser als die maximale Leistungsaufnahme ist, wird gar kein Überschuss mehr übergeben im PV Betrieb. + +
+
+
+
+
+

@@ -386,8 +408,12 @@ Hier ist das installierte Modell auszuwählen. @@ -397,7 +423,7 @@
- + Hier bitte die an den Acthor angeschlossene Leistung in Watt angeben. @@ -427,6 +453,25 @@
+
+
+
+
+ +
+ + + Bezieht sich auf die Modbusadresse 74, wie ist Überschuss zu übertragen.
+ Neue Möglichkeit -> Überschuss als positive Zahl übertragen, Bezug negativ
+ bisheriges Verhalten -> Überschuss als positive Zahl übertragen, Bezug als 0
+
+
+
+
+

@@ -537,7 +582,7 @@
-
+

@@ -731,8 +776,10 @@
- Parameter in % Ladezustand. 0% deaktiviert die Funktion. Bei deaktivierter Funktion oder wenn der Ladezustand grösser gleich dem Parameter ist, wird die Speicherleistung bei der Berechnung der Ein- und Ausschaltschwelle berücksichtigt.
- Unterhalb dieses Wertes ist für die Berechnung der Ein und Ausschaltschwelle nur die aktuelle Leistung am EVU Punkt und die maximal mögliche Speicherladung (als Offset) relevant.
+ Parameter in % Ladezustand. 0% deaktiviert die Funktion. Bei deaktivierter Funktion oder wenn der Ladezustand grösser gleich dem Parameter ist, wird die Speicherleistung bei der Berechnung der Ein- und Ausschaltschwelle berücksichtigt
Uberschuss = evu + speicherleistung, wobei evu - > Bezug(-)/Einspeisung(+) und speicherleistung Entladung(-)/Ladung(+) ist .
+ Unterhalb dieses Wertes ist für die Berechnung der obige Überschuss und die maximal mögliche Speicherladung (als Offset) relevant
Uberschussoffset = Uberschuss - maxspeicher
+ Bei überschussgesteuerten Geräten wird dann der Ueberschuss oder der Ueberschuss mit Offset übertragen. +
@@ -850,6 +897,7 @@ + @@ -861,7 +909,7 @@
-
+
@@ -915,13 +963,13 @@
-
+
-
+
@@ -929,6 +977,7 @@ Standardeinstellungen verschiedener Geräte:
SDM630/Lovato: 8899
Elgris: 502 + b23: ???
diff --git a/web/settings/topicsToSubscribe_smarthomeconfig.js b/web/settings/topicsToSubscribe_smarthomeconfig.js index b803b13a3..0d615f51e 100644 --- a/web/settings/topicsToSubscribe_smarthomeconfig.js +++ b/web/settings/topicsToSubscribe_smarthomeconfig.js @@ -84,6 +84,8 @@ var topicsToSubscribe = [ ["openWB/config/get/SmartHome/Devices/+/device_shauth", 0], ["openWB/config/get/SmartHome/Devices/+/device_measureshauth", 0], ["openWB/config/get/SmartHome/Devices/+/device_mindayeinschaltdauer", 0], - ["openWB/config/get/SmartHome/Devices/+/device_measuresmaage", 0] + ["openWB/config/get/SmartHome/Devices/+/device_measuresmaage", 0], + ["openWB/config/get/SmartHome/Devices/+/device_idmueb", 0], + ["openWB/config/get/SmartHome/Devices/+/device_maxueb", 0] ]; diff --git a/web/status/processAllMqttMsg.js b/web/status/processAllMqttMsg.js index 24db13ad9..43d434687 100644 --- a/web/status/processAllMqttMsg.js +++ b/web/status/processAllMqttMsg.js @@ -25,7 +25,7 @@ function getIndex(topic) { // since this is supposed to be the index like in openwb/lp/4/w // no lookbehind supported by safari, so workaround with replace needed var index = topic.match(/(?:\/)([0-9]+)(?=\/)/g)[0].replace(/[^0-9]+/g, ''); - if ( typeof index === 'undefined' ) { + if (typeof index === 'undefined') { index = ''; } return index; @@ -34,35 +34,35 @@ function getIndex(topic) { function handlevar(mqttmsg, mqttpayload) { // receives all messages and calls respective function to process them processPreloader(mqttmsg); - if ( mqttmsg.match( /^openwb\/lp\//i) ) { + if (mqttmsg.match(/^openwb\/lp\//i)) { processLpMsg(mqttmsg, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/evu\//i) ) { + else if (mqttmsg.match(/^openwb\/evu\//i)) { processEvuMsg(mqttmsg, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/pv\//i) ) { + else if (mqttmsg.match(/^openwb\/pv\//i)) { processPvMsg(mqttmsg, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/Verbraucher\//i) ) { + else if (mqttmsg.match(/^openwb\/Verbraucher\//i)) { processVerbraucherMsg(mqttmsg, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/housebattery\//i) ) { + else if (mqttmsg.match(/^openwb\/housebattery\//i)) { processBatMsg(mqttmsg, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/SmartHome\//i) ) { + else if (mqttmsg.match(/^openwb\/SmartHome\//i)) { processSmartHomeMsg(mqttmsg, mqttpayload); } - - else if ( mqttmsg.match( /^openwb\/global\//i) ) { + + else if (mqttmsg.match(/^openwb\/global\//i)) { processGlobalMsg(mqttmsg, mqttpayload); } else { - console.log("Unknown topic: "+mqttmsg+": "+mqttpayload); + console.log("Unknown topic: " + mqttmsg + ": " + mqttpayload); } } // end handlevar -function processGlobalMsg (mqttmsg, mqttpayload) { - switch(mqttmsg){ +function processGlobalMsg(mqttmsg, mqttpayload) { + switch (mqttmsg) { case "openWB/global/WAllChargePoints": directShow(mqttpayload, '#ladeleistungAll'); break; @@ -74,8 +74,8 @@ function processGlobalMsg (mqttmsg, mqttpayload) { } } -function processEvuMsg (mqttmsg, mqttpayload) { - switch(mqttmsg){ +function processEvuMsg(mqttmsg, mqttpayload) { + switch (mqttmsg) { case "openWB/evu/ASchieflast": directShow(mqttpayload, '#schieflastdiv'); break; @@ -136,16 +136,13 @@ function processEvuMsg (mqttmsg, mqttpayload) { } } -function processPvMsg (mqttmsg, mqttpayload) { - if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/.*$/i) ) - { +function processPvMsg(mqttmsg, mqttpayload) { + if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/.*$/i)) { var index = getIndex(mqttmsg); // extract number between two / / - if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/W$/i) ) - { + if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/W$/i)) { absShow(mqttpayload, '#inverter' + index + ' .powerInverter'); } - else if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/WhCounter$/i) ) - { + else if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/WhCounter$/i)) { kShow(mqttpayload, '#inverter' + index + ' .yieldInverter'); } // no data in openWB 1.x @@ -161,21 +158,18 @@ function processPvMsg (mqttmsg, mqttpayload) { // { // fractionDigitsShow(mqttpayload, '#inverter' + index + ' .yYieldInverter'); // } - else if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/faultState$/i) ) - { + else if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/faultState$/i)) { setWarningLevel(mqttpayload, '#inverter' + index + ' .faultStrPvRow'); } - else if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/faultStr$/i) ) - { + else if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/faultStr$/i)) { textShow(formatJsonString(mqttpayload), '#inverter' + index + ' .faultStrPv'); } - else if ( mqttmsg.match(/^openWB\/pv\/[0-9]+\/boolPVConfigured$/i) ) - { + else if (mqttmsg.match(/^openWB\/pv\/[0-9]+\/boolPVConfigured$/i)) { visibilityCard('#inverter' + index, mqttpayload); } } else { - switch(mqttmsg){ + switch (mqttmsg) { case "openWB/pv/CounterTillStartPvCharging": directShow(mqttpayload, '#pvcounterdiv'); break; @@ -190,16 +184,26 @@ function processPvMsg (mqttmsg, mqttpayload) { break; case "openWB/pv/MonthlyYieldKwh": fractionDigitsShow(mqttpayload, '#monthly_pvkwhdiv'); + if (mqttpayload != "0") { + showSection("#monatsertragRow"); + } else { + hideSection("#monatsertragRow"); + } break; case "openWB/pv/YearlyYieldKwh": fractionDigitsShow(mqttpayload, '#yearly_pvkwhdiv'); + if (mqttpayload != "0") { + showSection("#jahresertragRow"); + } else { + hideSection("#jahresertragRow"); + } break; } } } -function processBatMsg (mqttmsg, mqttpayload) { - switch(mqttmsg){ +function processBatMsg(mqttmsg, mqttpayload) { + switch (mqttmsg) { case "openWB/housebattery/boolHouseBatteryConfigured": visibilityCard('#speicher', mqttpayload); break; @@ -227,8 +231,8 @@ function processBatMsg (mqttmsg, mqttpayload) { } } -function processSmartHomeMsg (mqttmsg, mqttpayload) { - switch(mqttmsg){ +function processSmartHomeMsg(mqttmsg, mqttpayload) { + switch (mqttmsg) { case "openWB/SmartHome/Status/maxspeicherladung": directShow(mqttpayload, '#wmaxspeicherladung'); break; @@ -247,74 +251,74 @@ function processSmartHomeMsg (mqttmsg, mqttpayload) { } } -function processVerbraucherMsg (mqttmsg, mqttpayload) { +function processVerbraucherMsg(mqttmsg, mqttpayload) { var index = getIndex(mqttmsg); // extract number between two / / - if ( mqttmsg.match( /^openwb\/Verbraucher\/[1-2]\/Configured$/i ) ) { - visibilityCard('#loads'+index, mqttpayload); + if (mqttmsg.match(/^openwb\/Verbraucher\/[1-2]\/Configured$/i)) { + visibilityCard('#loads' + index, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/Verbraucher\/[1-2]\/Watt$/i ) ) { - directShow(mqttpayload, '#loads'+index+' .verbraucherWatt'); + else if (mqttmsg.match(/^openwb\/Verbraucher\/[1-2]\/Watt$/i)) { + directShow(mqttpayload, '#loads' + index + ' .verbraucherWatt'); } - else if ( mqttmsg.match( /^openwb\/Verbraucher\/[1-2]\/WhImported$/i ) ) { - kShow(mqttpayload, '#loads'+index+' .importVerbraucher'); + else if (mqttmsg.match(/^openwb\/Verbraucher\/[1-2]\/WhImported$/i)) { + kShow(mqttpayload, '#loads' + index + ' .importVerbraucher'); } - else if ( mqttmsg.match( /^openwb\/Verbraucher\/[1-2]\/WhExported$/i ) ) { - kShow(mqttpayload, '#loads'+index+' .exportVerbraucher'); + else if (mqttmsg.match(/^openwb\/Verbraucher\/[1-2]\/WhExported$/i)) { + kShow(mqttpayload, '#loads' + index + ' .exportVerbraucher'); } } -function processLpMsg (mqttmsg, mqttpayload) { +function processLpMsg(mqttmsg, mqttpayload) { var index = getIndex(mqttmsg); // extract number between two / / - if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/boolChargePointConfigured$/i ) ) { + if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/boolChargePointConfigured$/i)) { visibilityCard('#lp' + index, mqttpayload); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/APhase1$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/APhase1$/i)) { directShow(mqttpayload, '#lp' + index + ' .stromstaerkeP1'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/APhase2$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/APhase2$/i)) { directShow(mqttpayload, '#lp' + index + ' .stromstaerkeP2'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/APhase3$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/APhase3$/i)) { directShow(mqttpayload, '#lp' + index + ' .stromstaerkeP3'); - } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/AConfigured$/i ) ) { + } + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/AConfigured$/i)) { directShow(mqttpayload, '#lp' + index + ' .stromvorgabe'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/kWhCounter$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/kWhCounter$/i)) { fractionDigitsShow(mqttpayload, '#lp' + index + ' .kWhCounter'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/VPhase1$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/VPhase1$/i)) { directShow(mqttpayload, '#lp' + index + ' .spannungP1'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/VPhase2$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/VPhase2$/i)) { directShow(mqttpayload, '#lp' + index + ' .spannungP2'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/VPhase3$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/VPhase3$/i)) { directShow(mqttpayload, '#lp' + index + ' .spannungP3'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/W$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/W$/i)) { directShow(mqttpayload, '#lp' + index + ' .ladeleistung'); } - else if ( mqttmsg.match( /^openWB\/lp\/[1-9][0-9]*\/boolSocConfigured$/i )) { - if( mqttpayload == "1" ){ + else if (mqttmsg.match(/^openWB\/lp\/[1-9][0-9]*\/boolSocConfigured$/i)) { + if (mqttpayload == "1") { showSection('#lp' + index + ' .socRow'); } else { hideSection('#lp' + index + ' .socRow'); } } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/%Soc$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/%Soc$/i)) { directShow(mqttpayload, '#lp' + index + ' .soc'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/faultState$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/faultState$/i)) { setWarningLevel(mqttpayload, '#lp' + index + ' .faultStrLpRow'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/faultStr$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/faultStr$/i)) { textShow(formatJsonString(mqttpayload), '#lp' + index + ' .faultStrLp'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/socFaultState$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/socFaultState$/i)) { setWarningLevel(mqttpayload, '#lp' + index + ' .faultStrSocLpRow'); } - else if ( mqttmsg.match( /^openwb\/lp\/[1-9][0-9]*\/socFaultStr$/i ) ) { + else if (mqttmsg.match(/^openwb\/lp\/[1-9][0-9]*\/socFaultStr$/i)) { textShow(formatJsonString(mqttpayload), '#lp' + index + ' .faultStrSocLp'); } else { @@ -339,18 +343,18 @@ function processLpMsg (mqttmsg, mqttpayload) { // don't parse value function directShow(mqttpayload, variable) { - var value = parseFloat(mqttpayload); - if ( isNaN(value) ) { - value = 0; - } - var valueStr = value.toLocaleString(undefined) ; - $(variable).text(valueStr); + var value = parseFloat(mqttpayload); + if (isNaN(value)) { + value = 0; + } + var valueStr = value.toLocaleString(undefined); + $(variable).text(valueStr); } // show missing value or zero value as -- function noZeroShow(mqttpayload, variable) { var value = parseFloat(mqttpayload); - if ( isNaN(value) || (value == 0) ) { + if (isNaN(value) || (value == 0)) { valueStr = "--"; } else { @@ -364,7 +368,7 @@ function impExpShow(mqttpayload, variable) { // zur Anzeige Wert um "Bezug"/"Einspeisung" ergänzen var value = parseInt(mqttpayload); var valueStr = Math.abs(value).toLocaleString(undefined); - if(value < 0) { + if (value < 0) { valueStr += " (Exp.)"; } else if (value > 0) { valueStr += " (Imp.)"; @@ -376,24 +380,24 @@ function impExpShow(mqttpayload, variable) { function kShow(mqttpayload, variable) { var value = parseFloat(mqttpayload); value = (value / 1000); - var valueStr = value.toLocaleString(undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3}) ; + var valueStr = value.toLocaleString(undefined, { minimumFractionDigits: 3, maximumFractionDigits: 3 }); $(variable).text(valueStr); } // show absolute value (always >0) function absShow(mqttpayload, variable) { var value = Math.abs(parseInt(mqttpayload)); - var valueStr = value.toLocaleString(undefined) ; + var valueStr = value.toLocaleString(undefined); $(variable).text(valueStr); } //show kilo-payloads with 3 fraction digits function fractionDigitsShow(mqttpayload, variable) { var value = parseFloat(mqttpayload); - if ( isNaN(value) ) { + if (isNaN(value)) { value = 0; } - var valueStr = value.toLocaleString(undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3}); + var valueStr = value.toLocaleString(undefined, { minimumFractionDigits: 3, maximumFractionDigits: 3 }); $(variable).text(valueStr); } @@ -423,7 +427,7 @@ function setWarningLevel(mqttpayload, variable) { //Der String ist mit einem Tausender-Punkt versehen. Daher den Payload für die if-Abfrage verwenden. function visibilityMin(row, mqttpayload) { var value = parseFloat(mqttpayload) * -1; - if (value>100) { + if (value > 100) { showSection(row); } else { @@ -432,15 +436,15 @@ function visibilityMin(row, mqttpayload) { } //show/hide row with only one value -function visibilityValue(row, variable){ +function visibilityValue(row, variable) { var value = parseFloat($(variable).text()); // zu Berücksichtigung von 0,00 - if (( value != 0) && ( $(variable).text() != "")) { + if ((value != 0) && ($(variable).text() != "")) { showSection(row); } else { hideSection(row); } - var valueStr = value.toLocaleString(undefined, {minimumFractionDigits: 3, maximumFractionDigits: 3}); + var valueStr = value.toLocaleString(undefined, { minimumFractionDigits: 3, maximumFractionDigits: 3 }); $(variable).text(valueStr); } @@ -449,9 +453,9 @@ function visibilityRow(row, var1, var2, var3) { var val1 = parseFloat($(var1).text()); // zu Berücksichtigung von 0,00 var val2 = parseFloat($(var2).text()); var val3 = parseFloat($(var3).text()); - if ( ( (val1 == 0) || ($(var1).text() == "") ) && - ( (val2 == 0) || ($(var2).text() == "") ) && - ( (val3 == 0) || ($(var3).text() == "") ) ) { + if (((val1 == 0) || ($(var1).text() == "")) && + ((val2 == 0) || ($(var2).text() == "")) && + ((val3 == 0) || ($(var3).text() == ""))) { hideSection(row); } else { @@ -470,17 +474,17 @@ function visibilityCard(card, mqttpayload) { hideSection(card); } else { showSection(card); - if ( (card.match( /^[#]lp[2-8]$/i)) && lpGesCardShown == false ) { + if ((card.match(/^[#]lp[2-8]$/i)) && lpGesCardShown == false) { showSection('#lpges'); lpGesCardShown = true; - } else if ( card.match(/^[#]inverter[1-2]+$/i) ) { - if ( card == "#inverter1" ) { + } else if (card.match(/^[#]inverter[1-2]+$/i)) { + if (card == "#inverter1") { pv1 = mqttpayload; } else { pv2 = mqttpayload; } - if ( (pv1 + pv2) > 0 ) { + if ((pv1 + pv2) > 0) { showSection('#pvGes'); } else { hideSection('#pvGes'); diff --git a/web/status/status.php b/web/status/status.php index d59b68fc8..9164b72c9 100644 --- a/web/status/status.php +++ b/web/status/status.php @@ -364,11 +364,11 @@ function llanbindunglog() { Tagesertrag [kWh]
--
- + Monatsertrag [kWh]
--
- + Jahresertrag [kWh]
--
@@ -666,7 +666,7 @@ function processPreloader(mqttTopic) { // load mqtt library 'js/mqttws31.js', // functions for processing messages - 'status/processAllMqttMsg.js?ver=20210209', + 'status/processAllMqttMsg.js?ver=20230818', // functions performing mqtt and start mqtt-service 'status/setupMqttServices.js?ver=20210209', ]; diff --git a/web/themes/colors/powergraph.js b/web/themes/colors/powergraph.js index 0214362bb..0cd62e54c 100644 --- a/web/themes/colors/powergraph.js +++ b/web/themes/colors/powergraph.js @@ -566,7 +566,7 @@ class PowerGraph { values.lp0 = +elements[4]; values.lp1 = +elements[5]; for (i = 2; i < 9; i++) { - values["lp" + i] = +elements[11 + i]; + values["lp" + i] = +elements[12 + i]; } values.soc1 = +elements[9]; values.soc2 = +elements[10]; diff --git a/web/version b/web/version index 624e3c230..007017453 100644 --- a/web/version +++ b/web/version @@ -1 +1 @@ -1.9.303.0 +1.9.304.0