diff --git a/addressManager.py b/addressManager.py index 7e43fe0..1b567b4 100644 --- a/addressManager.py +++ b/addressManager.py @@ -66,8 +66,8 @@ class addressManager(): else: lines = result.stdout.split("\n") for line in lines: - if (line.find("inet6")>0): - k = line.find(" fe80::") # the beginning of the IPv6 + if (line.strip().find("inet6")>0): + k = line.strip().find("fe80::") # the beginning of the IPv6 if (k>0): sIpWithText = line[k+1:] x = sIpWithText.find(" ") # the space is the end of the IPv6 diff --git a/doc/pyPlc.ini.template b/doc/pyPlc.ini.template index a9e6fb9..4ffbd77 100644 --- a/doc/pyPlc.ini.template +++ b/doc/pyPlc.ini.template @@ -94,3 +94,7 @@ testsuite_enable = No # If this is activated, the pyPlc will send all logging messages also to the network interface, # in form of UDP Syslog messages. For details see in udplog.py. udp_syslog_enable = Yes + +# REST callback for SoC states. Comment out to disable. Do not leave a trailing slash +soc_callback_enabled = False +soc_callback_endpoint = http://1.1.1.1 \ No newline at end of file diff --git a/evseNoGui.py b/evseNoGui.py new file mode 100644 index 0000000..0ccb788 --- /dev/null +++ b/evseNoGui.py @@ -0,0 +1,46 @@ + +# The non-GUI variant of the PEV side + +import time +import pyPlcWorker +from configmodule import getConfigValue, getConfigValueBool +from pyPlcModes import * +import sys # for argv +import requests + +startTime_ms = round(time.time()*1000) + +def cbAddToTrace(s): + currentTime_ms = round(time.time()*1000) + dT_ms = currentTime_ms - startTime_ms + print("[" + str(dT_ms) + "ms] " + s) + +def cbShowStatus(s, selection=""): + pass + + +soc_callback_enabled = getConfigValueBool("soc_callback_enabled") +soc_callback_url = getConfigValue("soc_callback_endpoint") if soc_callback_enabled else "" + +def socStatusCallback(remaining_soc: int, full_soc: int = -1, bulk_soc: int = -1, origin: str = ""): + print(f"Received SoC status from {origin}.\n" + f" Remaining {remaining_soc}% \n" + f" Full at {full_soc}%\n" + f" Bulk at {bulk_soc}%") + if soc_callback_enabled: + requests.post(f"{soc_callback_url}/modem?remaining_soc={remaining_soc}&full_soc={full_soc}&bulk_soc={bulk_soc}") + +myMode = C_EVSE_MODE + +print("starting in EVSE_MODE") +print("press Ctrl-C to exit") + +worker=pyPlcWorker.pyPlcWorker(cbAddToTrace, cbShowStatus, myMode, 0, socStatusCallback) + +nMainloops=0 +while (1): + time.sleep(.03) # 'do some calculation' + nMainloops+=1 + worker.mainfunction() + +#--------------------------------------------------------------- diff --git a/fsmEvse.py b/fsmEvse.py index f18b0bb..879a051 100644 --- a/fsmEvse.py +++ b/fsmEvse.py @@ -7,9 +7,10 @@ import pyPlcTcpSocket import time # for time.sleep() from helpers import prettyHexMessage, combineValueAndMultiplier -from mytestsuite import * +from mytestsuite import * from random import random from exiConnector import * # for EXI data handling/converting +import requests stateWaitForSupportedApplicationProtocolRequest = 0 stateWaitForSessionSetupRequest = 1 @@ -27,19 +28,23 @@ class fsmEvse(): def publishStatus(self, s): self.callbackShowStatus(s, "evseState") - + + def publishSoCs(self, remaining_soc: int, full_soc: int = -1, bulk_soc: int = -1, origin: str = ""): + if self.callbackSoCStatus is not None: + self.callbackSoCStatus(remaining_soc, full_soc, bulk_soc, origin) + def enterState(self, n): self.addToTrace("from " + str(self.state) + " entering " + str(n)) if (self.state!=0) and (n==0): self.publishStatus("Waiting f AppHandShake") self.state = n self.cyclesInState = 0 - + def isTooLong(self): # The timeout handling function. return (self.cyclesInState > 100) # 100*33ms=3.3s - - + + def stateFunctionWaitForSupportedApplicationProtocolRequest(self): if (len(self.rxData)>0): self.addToTrace("In state WaitForSupportedApplicationProtocolRequest, received " + prettyHexMessage(self.rxData)) @@ -58,7 +63,7 @@ class fsmEvse(): self.Tcp.transmit(msg) self.publishStatus("Schema negotiated") self.enterState(stateWaitForSessionSetupRequest) - + def stateFunctionWaitForSessionSetupRequest(self): if (len(self.rxData)>0): self.simulatedPresentVoltage = 0 @@ -80,7 +85,7 @@ class fsmEvse(): self.enterState(stateWaitForServiceDiscoveryRequest) if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForServiceDiscoveryRequest(self): if (len(self.rxData)>0): self.addToTrace("In state WaitForServiceDiscoveryRequest, received " + prettyHexMessage(self.rxData)) @@ -100,7 +105,7 @@ class fsmEvse(): self.enterState(stateWaitForServicePaymentSelectionRequest) if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForServicePaymentSelectionRequest(self): if (len(self.rxData)>0): self.addToTrace("In state WaitForServicePaymentSelectionRequest, received " + prettyHexMessage(self.rxData)) @@ -120,7 +125,7 @@ class fsmEvse(): self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified. The Ioniq sends PowerDeliveryReq as next. if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForFlexibleRequest(self): if (len(self.rxData)>0): self.addToTrace("In state WaitForFlexibleRequest, received " + prettyHexMessage(self.rxData)) @@ -130,15 +135,26 @@ class fsmEvse(): self.addToTrace(strConverterResult) if (strConverterResult.find("PowerDeliveryReq")>0): # todo: check the request content, and fill response parameters + self.addToTrace("Received PowerDeliveryReq. Extracting SoC parameters") + info = json.loads(strConverterResult) + remaining_soc = int(info.get("EVRESSSOC", -1)) + self.publishSoCs(remaining_soc, origin="PowerDeliveryReq") msg = addV2GTPHeader(exiEncode("EDh")) # EDh for Encode, Din, PowerDeliveryResponse if (testsuite_faultinjection_is_triggered(TC_EVSE_ResponseCode_Failed_for_PowerDeliveryRes)): # send a PowerDeliveryResponse with Responsecode Failed msg = addV2GTPHeader("809a0125e6cecc51408420400000") self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("PowerDelivery") - self.Tcp.transmit(msg) + self.Tcp.transmit(msg) self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("ChargeParameterDiscoveryReq")>0): + self.addToTrace("Received ChargeParameterDiscoveryReq. Extracting SoC parameters via DC") + info = json.loads(strConverterResult) + remaining_soc = int(info.get("DC_EVStatus.EVRESSSOC", -1)) + full_soc = int(info.get("FullSOC", -1)) + bulk_soc = int(info.get("BulkSOC", -1)) + self.publishSoCs(remaining_soc, full_soc, bulk_soc, origin="ChargeParameterDiscoveryReq") + # todo: check the request content, and fill response parameters msg = addV2GTPHeader(exiEncode("EDe")) # EDe for Encode, Din, ChargeParameterDiscoveryResponse if (testsuite_faultinjection_is_triggered(TC_EVSE_ResponseCode_ServiceSelectionInvalid_for_ChargeParameterDiscovery)): @@ -146,11 +162,16 @@ class fsmEvse(): msg = addV2GTPHeader("809a0125e6cecd50810001ec00201004051828758405500080000101844138101c2432c04081436c900c0c000041435ecc044606000200") self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("ChargeParamDiscovery") - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("CableCheckReq")>0): # todo: check the request content, and fill response parameters # todo: make a real cable check, and while it is ongoing, send "Ongoing". + self.addToTrace("Received CableCheckReq. Extracting SoC parameters via DC") + info = json.loads(strConverterResult) + remaining_soc = int(info.get("DC_EVStatus.EVRESSSOC", -1)) + self.publishSoCs(remaining_soc, -1, -1, origin="CableCheckReq") + msg = addV2GTPHeader(exiEncode("EDf")) # EDf for Encode, Din, CableCheckResponse if (testsuite_faultinjection_is_triggered(TC_EVSE_ResponseCode_Failed_for_CableCheckRes)): # send a CableCheckResponse with Responsecode Failed @@ -158,7 +179,7 @@ class fsmEvse(): self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("CableCheck") if (not testsuite_faultinjection_is_triggered(TC_EVSE_Timeout_during_CableCheck)): - self.Tcp.transmit(msg) + self.Tcp.transmit(msg) self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("PreChargeReq")>0): # check the request content, and fill response parameters @@ -191,8 +212,8 @@ class fsmEvse(): self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("PreCharging " + strPresentVoltage) if (not testsuite_faultinjection_is_triggered(TC_EVSE_Timeout_during_PreCharge)): - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("ContractAuthenticationReq")>0): # todo: check the request content, and fill response parameters msg = addV2GTPHeader(exiEncode("EDl")) # EDl for Encode, Din, ContractAuthenticationResponse @@ -201,8 +222,8 @@ class fsmEvse(): msg = addV2GTPHeader("809a021a3b7c417774813310c0A200") self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("ContractAuthentication") - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("CurrentDemandReq")>0): # check the request content, and fill response parameters uTarget = 220 # default in case we cannot decode the requested voltage @@ -212,8 +233,14 @@ class fsmEvse(): strEVTargetVoltageMultiplier = y["EVTargetVoltage.Multiplier"] uTarget = combineValueAndMultiplier(strEVTargetVoltageValue, strEVTargetVoltageMultiplier) self.addToTrace("EV wants EVTargetVoltage " + str(uTarget)) - strSoc = y["DC_EVStatus.EVRESSSOC"] - self.callbackShowStatus(strSoc, "soc") + + remaining_soc = int(y.get("DC_EVStatus.EVRESSSOC", -1)) + full_soc = int(y.get("FullSOC", -1)) + bulk_soc = int(y.get("BulkSOC", -1)) + self.publishSoCs(remaining_soc, full_soc, bulk_soc, origin="CurrentDemandReq") + + self.callbackShowStatus(str(remaining_soc), "soc") + except: self.addToTrace("ERROR: Could not decode the CurrentDemandReq") self.simulatedPresentVoltage = uTarget + 3*random() # The charger provides the voltage which is demanded by the car. @@ -233,40 +260,40 @@ class fsmEvse(): self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("CurrentDemand") if (not testsuite_faultinjection_is_triggered(TC_EVSE_Timeout_during_CurrentDemand)): - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("WeldingDetectionReq")>0): # todo: check the request content, and fill response parameters msg = addV2GTPHeader(exiEncode("EDj")) # EDj for Encode, Din, WeldingDetectionRes self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("WeldingDetection") - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN if (strConverterResult.find("SessionStopReq")>0): # todo: check the request content, and fill response parameters msg = addV2GTPHeader(exiEncode("EDk")) # EDk for Encode, Din, SessionStopRes self.addToTrace("responding " + prettyHexMessage(msg)) self.publishStatus("SessionStop") - self.Tcp.transmit(msg) - self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + self.Tcp.transmit(msg) + self.enterState(stateWaitForFlexibleRequest) # todo: not clear, what is specified in DIN + - if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForChargeParameterDiscoveryRequest(self): if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForCableCheckRequest(self): if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForPreChargeRequest(self): if (self.isTooLong()): self.enterState(0) - + def stateFunctionWaitForPowerDeliveryRequest(self): if (len(self.rxData)>0): self.addToTrace("In state WaitForPowerDeliveryRequest, received " + prettyHexMessage(self.rxData)) @@ -275,9 +302,9 @@ class fsmEvse(): self.enterState(0) if (self.isTooLong()): self.enterState(0) - - - stateFunctions = { + + + stateFunctions = { stateWaitForSupportedApplicationProtocolRequest: stateFunctionWaitForSupportedApplicationProtocolRequest, stateWaitForSessionSetupRequest: stateFunctionWaitForSessionSetupRequest, stateWaitForServiceDiscoveryRequest: stateFunctionWaitForServiceDiscoveryRequest, @@ -288,15 +315,15 @@ class fsmEvse(): stateWaitForPreChargeRequest: stateFunctionWaitForPreChargeRequest, stateWaitForPowerDeliveryRequest: stateFunctionWaitForPowerDeliveryRequest, } - + def reInit(self): - self.addToTrace("re-initializing fsmEvse") + self.addToTrace("re-initializing fsmEvse") self.state = 0 self.cyclesInState = 0 self.rxData = [] self.simulatedPresentVoltage = 0 self.Tcp.resetTheConnection() - + def socketStateNotification(self, notification): if (notification==0): # The TCP informs us, that the connection is broken. @@ -311,12 +338,13 @@ class fsmEvse(): # The TCP informs us, that a connection is established. self.publishStatus("TCP connected") - def __init__(self, addressManager, callbackAddToTrace, hardwareInterface, callbackShowStatus): + def __init__(self, addressManager, callbackAddToTrace, hardwareInterface, callbackShowStatus, callbackSoCStatus = None): self.callbackAddToTrace = callbackAddToTrace self.callbackShowStatus = callbackShowStatus + self.callbackSoCStatus = callbackSoCStatus #todo self.addressManager = addressManager #todo self.hardwareInterface = hardwareInterface - self.addToTrace("initializing fsmEvse") + self.addToTrace("initializing fsmEvse") self.faultInjectionDelayUntilSocketOpen_s = 0 if (self.faultInjectionDelayUntilSocketOpen_s>0): self.addToTrace("Fault injection: waiting " + str(self.faultInjectionDelayUntilSocketOpen_s) + " s until opening the TCP socket.") @@ -325,7 +353,7 @@ class fsmEvse(): self.state = 0 self.cyclesInState = 0 self.rxData = [] - + def mainfunction(self): self.Tcp.mainfunction() # call the lower-level worker if (self.Tcp.isRxDataAvailable()): @@ -334,8 +362,8 @@ class fsmEvse(): # run the state machine: self.cyclesInState += 1 # for timeout handling, count how long we are in a state self.stateFunctions[self.state](self) - - + + if __name__ == "__main__": print("Testing the evse state machine") evse = fsmEvse() @@ -343,5 +371,5 @@ if __name__ == "__main__": while (True): time.sleep(0.1) evse.mainfunction() - + diff --git a/pyPlcHomeplug.py b/pyPlcHomeplug.py index d2aa682..dcf912e 100644 --- a/pyPlcHomeplug.py +++ b/pyPlcHomeplug.py @@ -1020,7 +1020,7 @@ class pyPlcHomeplug(): def findEthernetAdaptor(self): - self.strInterfaceName="eth0" # default, if the real is not found + self.strInterfaceName="eth1" # default, if the real is not found #print("Interfaces:\n" + '\n'.join(pcap.findalldevs())) for i in range(0, 10): strInterfaceName = pcap.ex_name("eth"+str(i)) diff --git a/pyPlcTcpSocket.py b/pyPlcTcpSocket.py index 7250ed1..ef41f0b 100644 --- a/pyPlcTcpSocket.py +++ b/pyPlcTcpSocket.py @@ -52,7 +52,7 @@ class pyPlcTcpClientSocket(): #print(host[0:5].lower()) if (host[0:5].lower()=="fe80:"): #print("This is a link local address. We need to add %eth0 at the end.") - host = host + "%eth0" + host = host + "%eth1" socket_addr = socket.getaddrinfo(host,port,socket.AF_INET6,socket.SOCK_DGRAM,socket.SOL_UDP)[0][4] #print(socket_addr) @@ -182,6 +182,10 @@ class pyPlcTcpServerSocket(): # in case of a broken connection, here we try to start it again self.addToTrace("Trying to reset the TCP socket") # Todo: how to "reset" the socket? + try: + self.ourSocket.close() + except: + pass self.ourSocket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, 0) self.ourSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ourSocket.bind((self.ipAdress, self.tcpPort)) @@ -297,7 +301,7 @@ def testClientSocket(): #c.connect('fe80::e0ad:99ac:52eb:85d3', 15118) #c.connect('fe80::e0ad:99ac:52eb:9999', 15118) #c.connect('localhost', 15118) - c.connect('fe80::c690:83f3:fbcb:980e', 15118) + c.connect('fe80::407d:89f9:f6c2:6ee0', 15118) print("connected="+str(c.isConnected)) if (c.isConnected): print("sending something to the server") diff --git a/pyPlcWorker.py b/pyPlcWorker.py index 76a0a08..7e92a92 100644 --- a/pyPlcWorker.py +++ b/pyPlcWorker.py @@ -15,17 +15,18 @@ import time import subprocess import hardwareInterface import connMgr - + class pyPlcWorker(): - def __init__(self, callbackAddToTrace=None, callbackShowStatus=None, mode=C_EVSE_MODE, isSimulationMode=0): - print("initializing pyPlcWorker") + def __init__(self, callbackAddToTrace=None, callbackShowStatus=None, mode=C_EVSE_MODE, isSimulationMode=0, callbackSoC=None): + print("initializing pyPlcWorker") self.nMainFunctionCalls=0 self.mode = mode self.strUserAction = "" self.addressManager = addressManager.addressManager() self.callbackAddToTrace = callbackAddToTrace self.callbackShowStatus = callbackShowStatus + self.callbackSoC = callbackSoC self.oldAvlnStatus = 0 self.isSimulationMode = isSimulationMode self.connMgr = connMgr.connMgr(self.workerAddToTrace, self.showStatus) @@ -38,9 +39,9 @@ class pyPlcWorker(): strLabel = str(subprocess.check_output(["git", "describe", "--tags"], text=True).strip()) except: strLabel = "(unknown version. 'git describe --tags' failed.)" - self.workerAddToTrace("[pyPlcWorker] Software version " + strLabel) + self.workerAddToTrace("[pyPlcWorker] Software version " + strLabel) if (self.mode == C_EVSE_MODE): - self.evse = fsmEvse.fsmEvse(self.addressManager, self.workerAddToTrace, self.hardwareInterface, self.showStatus) + self.evse = fsmEvse.fsmEvse(self.addressManager, self.workerAddToTrace, self.hardwareInterface, self.showStatus, self.callbackSoC) if (self.mode == C_PEV_MODE): self.pev = fsmPev.fsmPev(self.addressManager, self.connMgr, self.workerAddToTrace, self.hardwareInterface, self.showStatus) def __del__(self): @@ -49,14 +50,14 @@ class pyPlcWorker(): del(self.pev) except: pass - + def workerAddToTrace(self, s): # The central logging function. All logging messages from the different parts of the project # shall come here. #print("workerAddToTrace " + s) self.callbackAddToTrace(s) # give the message to the upper level, eg for console log. self.hp.printToUdp(s) # give the message to the udp for remote logging. - + def showStatus(self, s, selection = "", strAuxInfo1="", strAuxInfo2=""): self.callbackShowStatus(s, selection) if (selection == "pevState"): @@ -70,7 +71,7 @@ class pyPlcWorker(): return if (self.connMgr.getConnectionLevel()<50): self.oldAvlnStatus = 0 - + def mainfunction(self): self.nMainFunctionCalls+=1 #self.showStatus("pyPlcWorker loop " + str(self.nMainFunctionCalls)) @@ -83,7 +84,7 @@ class pyPlcWorker(): self.evse.mainfunction() # call the evse state machine if (self.mode == C_PEV_MODE): self.pev.mainfunction() # call the pev state machine - + def handleUserAction(self, strAction): self.strUserAction = strAction print("user action " + strAction) @@ -112,7 +113,7 @@ class pyPlcWorker(): if (strAction == "L"): print("switching to LISTEN mode") self.mode = C_LISTEN_MODE - self.hp.enterListenMode() + self.hp.enterListenMode() if (hasattr(self, 'evse')): print("deleting evse") del self.evse