diff --git a/.gitignore b/.gitignore index 18b5ad4..d125da6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ testresults.txt /log /local *.bak +*.pyc + diff --git a/doc/pyPlc.ini.template b/doc/pyPlc.ini.template index 8a6d71d..9c17037 100644 --- a/doc/pyPlc.ini.template +++ b/doc/pyPlc.ini.template @@ -99,6 +99,12 @@ testsuite_enable = No # in form of UDP Syslog messages. For details see in udplog.py. udp_syslog_enable = Yes +# Set backend for obtaining charging parameters, we start with CHAdeMO CAN for now +# Need to make a simulator device and maybe a celeron device? +charge_parameter_backend = chademo + # 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 +# Fallback value to use if the vehicle does not support the EVEnergyCapacity.Value +soc_fallback_energy_capacity = 2700 diff --git a/evseNoGui.py b/evseNoGui.py index dcf1446..24d4f31 100644 --- a/evseNoGui.py +++ b/evseNoGui.py @@ -21,14 +21,38 @@ def cbShowStatus(s, selection=""): soc_callback_enabled = getConfigValueBool("soc_callback_enabled") soc_callback_url = getConfigValue("soc_callback_endpoint") if soc_callback_enabled else "" +soc_fallback_energy_capacity = getConfigValue("soc_fallback_energy_capacity") + +def socStatusCallback(current_soc: int, full_soc: int = -1, energy_capacity: int = -1, energy_request: int = -1, evccid: str = "", origin: str = ""): + # Do some basic value checks and conversions + # Some cars do not support certain values and return 0, make sure we actually send -1 + + if (energy_capacity > 0): + # We need Wh, not something in between kWh and Wh + energy_capacity = energy_capacity * 10 + else: + # Some cars do not supply energy capacity of their battery + # Support some kind of fallback value which would work for installations where typically one car charges + if (int(soc_fallback_energy_capacity) > 0): + energy_capacity = int(soc_fallback_energy_capacity) * 10 + else: + energy_capacity = -1 + + if (energy_request > 0): + # We need Wh, not something in between kWh and Wh + energy_request = energy_request * 10 + else: + energy_request = -1 -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}%") + f" Current SoC {current_soc}% \n" + f" Full SoC {full_soc}%\n" + f" Energy capacity {energy_capacity} Wh \n" + f" Energy request {energy_request} Wh \n" + f" EVCCID {evccid} \n") + if soc_callback_enabled: - requests.post(f"{soc_callback_url}/modem?remaining_soc={remaining_soc}&full_soc={full_soc}&bulk_soc={bulk_soc}") + requests.post(f"{soc_callback_url}?current_soc={current_soc}&full_soc={full_soc}&energy_capacity={energy_capacity}&energy_request={energy_request}&evccid={evccid}") myMode = C_EVSE_MODE diff --git a/fsmEvse.py b/fsmEvse.py index d759ca3..38ace4a 100644 --- a/fsmEvse.py +++ b/fsmEvse.py @@ -28,9 +28,9 @@ 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 = ""): + def publishSoCs(self, current_soc: int, full_soc: int = -1, energy_capacity: int = -1, energy_request: int = -1, evccid: str = "", origin: str = ""): if self.callbackSoCStatus is not None: - self.callbackSoCStatus(remaining_soc, full_soc, bulk_soc, origin) + self.callbackSoCStatus(current_soc, full_soc, energy_capacity, energy_request, self.evccid, origin) def enterState(self, n): self.addToTrace("from " + str(self.state) + " entering " + str(n)) @@ -82,6 +82,9 @@ class fsmEvse(): self.Tcp.transmit(msg) self.publishStatus("Session established") self.enterState(stateWaitForServiceDiscoveryRequest) + y = json.loads(strConverterResult) + self.evccid = y.get("EVCCID", "") + if (self.isTooLong()): self.enterState(0) @@ -136,8 +139,8 @@ class fsmEvse(): # 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") + current_soc = int(info.get("EVRESSSOC", -1)) + self.publishSoCs(current_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 @@ -149,10 +152,11 @@ class fsmEvse(): 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)) + current_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") + energy_capacity = int(info.get("EVEnergyCapacity.Value", -1)) + energy_request = int(info.get("EVEnergyRequest.Value", -1)) + self.publishSoCs(current_soc, full_soc, energy_capacity, energy_request, origin="ChargeParameterDiscoveryReq") # todo: check the request content, and fill response parameters msg = addV2GTPHeader(exiEncode("EDe")) # EDe for Encode, Din, ChargeParameterDiscoveryResponse @@ -168,8 +172,8 @@ class fsmEvse(): # 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") + current_soc = int(info.get("DC_EVStatus.EVRESSSOC", -1)) + self.publishSoCs(current_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)): @@ -232,13 +236,14 @@ class fsmEvse(): strEVTargetVoltageMultiplier = y["EVTargetVoltage.Multiplier"] uTarget = combineValueAndMultiplier(strEVTargetVoltageValue, strEVTargetVoltageMultiplier) self.addToTrace("EV wants EVTargetVoltage " + str(uTarget)) - - remaining_soc = int(y.get("DC_EVStatus.EVRESSSOC", -1)) + current_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") + energy_capacity = int(y.get("EVEnergyCapacity.Value", -1)) + energy_request = int(y.get("EVEnergyRequest.Value", -1)) - self.callbackShowStatus(str(remaining_soc), "soc") + self.publishSoCs(current_soc, full_soc, energy_capacity, energy_request, origin="CurrentDemandReq") + + self.callbackShowStatus(str(current_soc), "soc") except: self.addToTrace("ERROR: Could not decode the CurrentDemandReq") @@ -352,6 +357,7 @@ class fsmEvse(): self.state = 0 self.cyclesInState = 0 self.rxData = [] + self.evccid = "" def mainfunction(self): self.Tcp.mainfunction() # call the lower-level worker diff --git a/fsmPev.py b/fsmPev.py index e1da261..61acb26 100644 --- a/fsmPev.py +++ b/fsmPev.py @@ -425,6 +425,10 @@ class fsmPev(): if (strConverterResult.find('"EVSEProcessing": "Finished"')>0): self.publishStatus("ChargeParams discovered") self.addToTrace("Checkpoint550: ChargeParams are discovered. Will change to state C.") + #Report charger paramters + maxI = combineValueAndMultiplier(y["EVSEMaximumCurrentLimit.Value"], y["EVSEMaximumCurrentLimit.Multiplier"]) + maxV = combineValueAndMultiplier(y["EVSEMaximumVoltageLimit.Value"], y["EVSEMaximumVoltageLimit.Multiplier"]) + self.hardwareInterface.setChargerParameters(maxV, maxI) # pull the CP line to state C here: self.hardwareInterface.setStateC() self.addToTrace("Checkpoint555: Locking the connector.") @@ -650,9 +654,13 @@ class fsmPev(): strResponseCode = y["ResponseCode"] strEVSEPresentVoltageValue = y["EVSEPresentVoltage.Value"] strEVSEPresentVoltageMultiplier = y["EVSEPresentVoltage.Multiplier"] + strEVSEPresentCurrentValue = y["EVSEPresentCurrent.Value"] + strEVSEPresentCurrentMultiplier = y["EVSEPresentCurrent.Multiplier"] u = combineValueAndMultiplier(strEVSEPresentVoltageValue, strEVSEPresentVoltageMultiplier) + i = combineValueAndMultiplier(strEVSEPresentCurrentValue, strEVSEPresentCurrentMultiplier) self.callbackShowStatus(format(u,".1f"), "EVSEPresentVoltage") strEVSEStatusCode = y["DC_EVSEStatus.EVSEStatusCode"] + self.hardwareInterface.setChargerVoltageAndCurrent(u, i) except: self.addToTrace("ERROR: Could not decode the PreChargeResponse") if (strResponseCode!="OK"): diff --git a/hardwareInterface.py b/hardwareInterface.py index 0263437..205bcfc 100644 --- a/hardwareInterface.py +++ b/hardwareInterface.py @@ -16,6 +16,10 @@ if (getConfigValue("digital_output_device")=="beaglebone"): # In case we run on beaglebone, we want to use GPIO ports. import Adafruit_BBIO.GPIO as GPIO +if (getConfigValue("charge_parameter_backend")=="chademo"): + # In case we use the CHAdeMO backend, we want to use CAN + import can + class hardwareInterface(): def needsSerial(self): # Find out, whether we need a serial port. This depends on several configuration items. @@ -140,6 +144,14 @@ class hardwareInterface(): #if (getConfigValue("digital_output_device")=="celeron55device"): # return self.lock_confirmed return 1 # todo: use the real connector lock feedback + + def setChargerParameters(self, maxVoltage, maxCurrent): + self.maxChargerVoltage = int(maxVoltage) + self.maxChargerCurrent = int(maxCurrent) + + def setChargerVoltageAndCurrent(self, voltageNow, currentNow): + self.chargerVoltage = int(voltageNow) + self.chargerCurrent = int(currentNow) def getInletVoltage(self): # uncomment this line, to take the simulated inlet voltage instead of the really measured @@ -149,6 +161,8 @@ class hardwareInterface(): def getAccuVoltage(self): if (getConfigValue("digital_output_device")=="celeron55device"): return self.accuVoltage + elif getConfigValue("charge_parameter_backend")=="chademo": + return self.accuVoltage #todo: get real measured voltage from the accu self.accuVoltage = 230 return self.accuVoltage @@ -161,13 +175,17 @@ class hardwareInterface(): if self.accuMaxCurrent >= EVMaximumCurrentLimit: return EVMaximumCurrentLimit return self.accuMaxCurrent + elif getConfigValue("charge_parameter_backend")=="chademo": + return self.accuMaxCurrent #set by CAN #todo: get max charging current from the BMS self.accuMaxCurrent = 10 return self.accuMaxCurrent def getAccuMaxVoltage(self): - if getConfigValue("charge_target_voltage"): - self.accuMaxVoltage = getConfigValue("charge_target_voltage") + if getConfigValue("charge_parameter_backend")=="chademo": + return self.accuMaxVoltage #set by CAN + elif getConfigValue("charge_target_voltage"): + self.accuMaxVoltage = getConfigValue("charge_target_voltage") else: #todo: get max charging voltage from the BMS self.accuMaxVoltage = 230 @@ -191,6 +209,13 @@ class hardwareInterface(): return self.simulatedSoc def initPorts(self): + if (getConfigValue("charge_parameter_backend") == "chademo"): + filters = [ + {"can_id": 0x100, "can_mask": 0x7FF, "extended": False}, + {"can_id": 0x101, "can_mask": 0x7FF, "extended": False}, + {"can_id": 0x102, "can_mask": 0x7FF, "extended": False}] + self.canbus = can.interface.Bus(bustype='socketcan', channel="can0", can_filters = filters) + if (getConfigValue("digital_output_device") == "beaglebone"): # Port configuration according to https://github.com/jsphuebner/pyPLC/commit/475f7fe9f3a67da3d4bd9e6e16dfb668d0ddb1d6 GPIO.setup("P8_16", GPIO.OUT) #output for port relays @@ -209,10 +234,17 @@ class hardwareInterface(): self.lock_confirmed = False # Confirmation from hardware self.cp_pwm = 0.0 self.soc_percent = 0.0 + self.capacity = 0.0 + self.accuMaxVoltage = 0.0 self.accuMaxCurrent = 0.0 self.contactor_confirmed = False # Confirmation from hardware self.plugged_in = None # None means "not known yet" + self.maxChargerVoltage = 0 + self.maxChargerCurrent = 10 + self.chargerVoltage = 0 + self.chargerCurrent = 0 + self.logged_inlet_voltage = None self.logged_dc_link_voltage = None self.logged_cp_pwm = None @@ -336,6 +368,9 @@ class hardwareInterface(): # 0.5 charging needs ~8s, good for automatic test case runs. self.simulatedSoc = self.simulatedSoc + deltaSoc + if (getConfigValue("charge_parameter_backend")=="chademo"): + self.mainfunction_chademo() + if (getConfigValue("digital_output_device")=="dieter"): self.mainfunction_dieter() @@ -376,6 +411,43 @@ class hardwareInterface(): s = "" # for the case we received corrupted data (not convertable as utf-8) #self.addToTrace(str(len(s)) + " bytes received: " + s) self.evaluateReceivedData_celeron55device(s) + + def mainfunction_chademo(self): + message = self.canbus.recv(0) + + if message: + if message.arbitration_id == 0x100: + vtg = (message.data[1] << 8) + message.data[0] + if self.accuVoltage != vtg: + self.addToTrace("CHAdeMO: Set battery voltage to %d V" % vtg) + self.accuVoltage = vtg + if self.capacity != message.data[6]: + self.addToTrace("CHAdeMO: Set capacity to %d" % message.data[6]) + self.capacity = message.data[6] + + msg = can.Message(arbitration_id=0x108, data=[ 0, self.maxChargerVoltage & 0xFF, self.maxChargerVoltage >> 8, self.maxChargerCurrent, 0, 0, 0, 0], is_extended_id=False) + self.canbus.send(msg) + #Report unspecified version 10, this makes our custom implementation send the momentary + #battery voltage in 0x100 bytes 0 and 1 + status = 4 if self.maxChargerVoltage > 0 else 0 #report connector locked + msg = can.Message(arbitration_id=0x109, data=[ 10, self.chargerVoltage & 0xFF, self.chargerVoltage >> 8, self.chargerCurrent, 0, status, 0, 0], is_extended_id=False) + self.canbus.send(msg) + + if message.arbitration_id == 0x102: + vtg = (message.data[2] << 8) + message.data[1] + if self.accuMaxVoltage != vtg: + self.addToTrace("CHAdeMO: Set target voltage to %d V" % vtg) + self.accuMaxVoltage = vtg + + if self.accuMaxCurrent != message.data[3]: + self.addToTrace("CHAdeMO: Set current request to %d A" % message.data[3]) + self.accuMaxCurrent = message.data[3] + + if self.capacity > 0: + soc = message.data[6] / self.capacity * 100 + if self.simulatedSoc != soc: + self.addToTrace("CHAdeMO: Set SoC to %d %%" % soc) + self.simulatedSoc = soc def myPrintfunction(s): print("myprint " + s)