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'):
+ # Überschuss als positive Zahl übertragen, Bezug negativ
+ # Überschuss als positive Zahl übertragen, Bezug als 0
+ 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() {
value="soc_leaf">Nissan
value="soc_psa">PSA (Peugeot/Citroen/DS/Opel/Vauxhall)
value="soc_zoe">Renault Zoe (alt)
- value="soc_smarteq">smart EQ
+ value="soc_ovms">OVMS
value="soc_tesla">Tesla
value="soc_vag">VAG
value="soc_volvo">Volvo
@@ -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
+
+
+
Geräteadresse
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 @@
Kein Gerät
Shelly oder Shelly plus
Tasmota
- Acthor
+ Acthor oder Elwa2
Lambda
Elwa
Idm
@@ -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 @@
N4Dac02
DA02
M120T von Pigeon
+ AA02B
Hier ist das installierte Modell auszuwählen.
@@ -377,6 +379,26 @@
+
+
-
+
-
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 @@
Json
MyStrom
SDM630
+
b23
Shelly oder Shelly plus
tasmota
WE514
@@ -861,7 +909,7 @@
-