This commit is contained in:
uhi22 2023-06-27 19:57:11 +02:00
commit 12d3d021bf
6 changed files with 139 additions and 21 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ testresults.txt
/log
/local
*.bak
*.pyc

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"):

View file

@ -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)