Merge pull request #33 from jsphuebner/mqtt_backend

Mqtt backend
This commit is contained in:
Uwe Hennig 2024-11-04 17:33:24 +01:00 committed by GitHub
commit 8293b77789
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 23 deletions

View file

@ -44,6 +44,10 @@ light_bulb_demo = no
# Possible values: yes or no
soc_simulation = yes
# Precharge simulation
# In EVSE mode simulate precharging rather than using physical value
evse_simulate_precharge = yes
# Device selection for the digital outputs, for CP state and power relays
# Possible options:
# dieter: Serial controlled device, which controls the digital outputs. E.g. arduino from https://github.com/uhi22/dieter
@ -52,6 +56,7 @@ soc_simulation = yes
digital_output_device = dieter
#digital_output_device = beaglebone
#digital_output_device = celeron55device
#digital_output_device = mqtt
# Device to read the physically measured inlet voltage in PevMode
@ -62,6 +67,7 @@ digital_output_device = dieter
#analog_input_device = dieter
analog_input_device = none
#analog_input_device = celeron55device
#analog_input_device = mqtt
# Criteria for ending the PreCharge phase in PevMode
@ -89,6 +95,9 @@ u_delta_max_for_end_of_precharge = 10
# auto
serial_port = auto
serial_baud = 19200
# MQTT broker and base topic in case MQTT is used
mqtt_broker = localhost
mqtt_topic = pyPlc
# The target voltage used in the CurrentDemandRequest.
# This is a value for first try-outs. Better would
@ -131,10 +140,13 @@ 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?
# Possible values:
# mqtt: pyPLC is used as a bridge between an MQTT broker and the car or EVSE
# chademo: pyPLC is used as bridge between a CCS charger and a CHAdeMO* car.
# Limitations/explanations here: https://openinverter.org/forum/viewtopic.php?p=57894#p57894 and
# https://openinverter.org/forum/viewtopic.php?t=1063 (Is it possible to make a CCS to CHAdeMO adapter?)
# none: all other use cases
#charge_parameter_backend = chademo
#charge_parameter_backend = mqtt
charge_parameter_backend = none
# REST callback for SoC states. Comment out to disable. Do not leave a trailing slash

View file

@ -27,6 +27,7 @@ class fsmEvse():
def publishStatus(self, s):
self.callbackShowStatus(s, "evseState")
self.hardwareInterface.displayState(s)
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:
@ -262,7 +263,12 @@ class fsmEvse():
self.simulatedPresentVoltage += 20
if (self.simulatedPresentVoltage<uTarget):
self.simulatedPresentVoltage += 5
if getConfigValueBool('evse_simulate_precharge'):
strPresentVoltage = str(int(self.simulatedPresentVoltage*10)/10) # "345"
else:
strPresentVoltage = str(self.hardwareInterface.getInletVoltage())
# in case we control a real power supply: give the precharge target to it
self.hardwareInterface.setPowerSupplyVoltageAndCurrent(uTarget, 1)
self.callbackShowStatus(strPresentVoltage, "EVSEPresentVoltage")
@ -322,7 +328,8 @@ class fsmEvse():
strEVTargetCurrentValue = jsondict["EVTargetCurrent.Value"]
strEVTargetCurrentMultiplier = jsondict["EVTargetCurrent.Multiplier"]
iTarget = combineValueAndMultiplier(strEVTargetCurrentValue, strEVTargetCurrentMultiplier)
self.addToTrace("EV wants EVTargetVoltage " + str(uTarget))
self.addToTrace("EV wants EVTargetVoltage " + str(uTarget) + " and EVTargetCurrent " + str(iTarget))
self.hardwareInterface.setPowerSupplyVoltageAndCurrent(uTarget, iTarget)
current_soc = int(jsondict.get("DC_EVStatus.EVRESSSOC", -1))
full_soc = int(jsondict.get("FullSOC", -1))
energy_capacity = int(jsondict.get("EVEnergyCapacity.Value", -1))
@ -336,9 +343,9 @@ class fsmEvse():
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.
strPresentVoltage = str(int(self.simulatedPresentVoltage*10)/10) # "345"
strPresentVoltage = str(self.hardwareInterface.getInletVoltage()) #str(self.simulatedPresentVoltage)
self.callbackShowStatus(strPresentVoltage, "EVSEPresentVoltage")
strEVSEPresentCurrent = "1" # Just as a dummy current
strEVSEPresentCurrent = str(self.hardwareInterface.getAccuMaxCurrent()) #"1" # Just as a dummy current
if (self.blChargeStopTrigger == 1):
# User pressed the STOP button on the charger. Send EVSE_Shutdown.
self.addToTrace("User pressed the STOP button on the charger. Sending EVSE_Shutdown.")

View file

@ -286,6 +286,7 @@ class fsmPev():
self.sessionId = strSessionId
except:
self.addToTrace("ERROR: Could not decode the sessionID")
if ((strResponseCode!="OK_NewSessionEstablished") and (strResponseCode!="OK")):
# According to the standard, the only valid response code is OK_NewSessionEstablished.
# But the ABB chargers use "OK", so we need to accept this, too. Discussed

View file

@ -12,8 +12,12 @@ from time import sleep, time
from configmodule import getConfigValue, getConfigValueBool
import sys # For exit_on_session_end hack
PinCp = "P8_18"
PinPowerRelay = "P8_16"
PinCp = "P9_41"
PinPowerRelay = "P9_17"
if (getConfigValue("digital_output_device")=="mqtt"):
# If we use MQTT as a hardware interface import it here
import paho.mqtt.client as mqtt
if (getConfigValue("digital_output_device")=="beaglebone"):
# In case we run on beaglebone, we want to use GPIO ports.
@ -85,12 +89,18 @@ class hardwareInterface():
def addToTrace(self, s):
self.callbackAddToTrace("[HARDWAREINTERFACE] " + s)
def displayState(self, state):
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/fsm_state", state)
def setStateB(self):
self.addToTrace("Setting CP line into state B.")
if (getConfigValue("digital_output_device")=="beaglebone"):
GPIO.output(PinCp, GPIO.LOW)
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("cp=0\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/cpstate", "B")
self.outvalue &= ~1
def setStateC(self):
@ -99,6 +109,8 @@ class hardwareInterface():
GPIO.output(PinCp, GPIO.HIGH)
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("cp=1\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/cpstate", "C")
self.outvalue |= 1
def setPowerRelayOn(self):
@ -107,6 +119,8 @@ class hardwareInterface():
GPIO.output(PinPowerRelay, GPIO.HIGH)
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("contactor=1\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/relay_request", "on")
self.outvalue |= 2
def setPowerRelayOff(self):
@ -115,6 +129,8 @@ class hardwareInterface():
GPIO.output(PinPowerRelay, GPIO.LOW)
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("contactor=0\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/relay_request", "off")
self.outvalue &= ~2
def setRelay2On(self):
@ -134,12 +150,16 @@ class hardwareInterface():
self.addToTrace("Locking the connector")
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("lock\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/lock_request", "lock")
# todo control the lock motor into lock direction until the end (time based or current based stopping?)
def triggerConnectorUnlocking(self):
self.addToTrace("Unocking the connector")
self.addToTrace("Unlocking the connector")
if (getConfigValue("digital_output_device")=="celeron55device"):
self.ser.write(bytes("unlock\n", "utf-8"))
if (getConfigValue("digital_output_device")=="mqtt"):
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/lock_request", "unlock")
# todo control the lock motor into unlock direction until the end (time based or current based stopping?)
def isConnectorLocked(self):
@ -149,16 +169,29 @@ class hardwareInterface():
return 1 # todo: use the real connector lock feedback
def setChargerParameters(self, maxVoltage, maxCurrent):
self.addToTrace("Setting charger parameters maxVoltage=%d V, maxCurrent=%d A" % (maxVoltage, maxCurrent))
self.maxChargerVoltage = int(maxVoltage)
self.maxChargerCurrent = int(maxCurrent)
if getConfigValue("charge_parameter_backend") == "mqtt":
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/charger_max_voltage", str(maxVoltage))
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/charger_max_current", str(maxCurrent))
def setChargerVoltageAndCurrent(self, voltageNow, currentNow):
self.addToTrace("Setting charger present values Voltage=%d V, Current=%d A" % (voltageNow, currentNow))
self.chargerVoltage = int(voltageNow)
self.chargerCurrent = int(currentNow)
if getConfigValue("charge_parameter_backend") == "mqtt":
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/charger_voltage", voltageNow)
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/charger_current", currentNow)
def setPowerSupplyVoltageAndCurrent(self, targetVoltage, targetCurrent):
# if we are the charger, and have a real power supply which we want to control, we do it here
self.homeplughandler.sendSpecialMessageToControlThePowerSupply(targetVoltage, targetCurrent)
#here we can publish the voltage and current requests received from the PEV side
if getConfigValue("charge_parameter_backend") == "mqtt":
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/pev_voltage", str(targetVoltage))
self.mqttclient.publish(getConfigValue("mqtt_topic") + "/pev_current", str(targetVoltage))
def getInletVoltage(self):
# uncomment this line, to take the simulated inlet voltage instead of the really measured
@ -166,9 +199,7 @@ class hardwareInterface():
return self.inletVoltage
def getAccuVoltage(self):
if (getConfigValue("digital_output_device")=="celeron55device"):
return self.accuVoltage
elif getConfigValue("charge_parameter_backend")=="chademo":
if getConfigValue("charge_parameter_backend") in ["chademo", "mqtt", "celeron55device"]:
return self.accuVoltage
#todo: get real measured voltage from the accu
self.accuVoltage = 230
@ -182,15 +213,15 @@ class hardwareInterface():
if self.accuMaxCurrent >= EVMaximumCurrentLimit:
return EVMaximumCurrentLimit
return self.accuMaxCurrent
elif getConfigValue("charge_parameter_backend")=="chademo":
return self.accuMaxCurrent #set by CAN
elif getConfigValue("charge_parameter_backend") in ["chademo", "mqtt"]:
return self.accuMaxCurrent #set by CAN or MQTT
#todo: get max charging current from the BMS
self.accuMaxCurrent = 10
return self.accuMaxCurrent
def getAccuMaxVoltage(self):
if getConfigValue("charge_parameter_backend")=="chademo":
return self.accuMaxVoltage #set by CAN
if getConfigValue("charge_parameter_backend") in ["chademo", "mqtt"]:
return self.accuMaxVoltage #set by CAN or MQTT
elif getConfigValue("charge_target_voltage"):
self.accuMaxVoltage = getConfigValue("charge_target_voltage")
else:
@ -209,7 +240,7 @@ class hardwareInterface():
def getSoc(self):
if self.callbackShowStatus:
self.callbackShowStatus(format(self.soc_percent,".1f"), "soc")
if (getConfigValue("digital_output_device")=="celeron55device"):
if (getConfigValue("digital_output_device") in ["celeron55device", "mqtt"]):
return self.soc_percent
#todo: get SOC from the BMS
self.callbackShowStatus(format(self.simulatedSoc,".1f"), "soc")
@ -240,6 +271,12 @@ class hardwareInterface():
GPIO.setup(PinPowerRelay, GPIO.OUT) #output for port relays
GPIO.setup(PinCp, GPIO.OUT) #output for CP
if (getConfigValue("digital_output_device") == "mqtt"):
self.mqttclient = mqtt.Client()
self.mqttclient.on_connect = self.mqtt_on_connect
self.mqttclient.on_message = self.mqtt_on_message
self.mqttclient.connect(getConfigValue("mqtt_broker"), 1883, 60)
def __init__(self, callbackAddToTrace=None, callbackShowStatus=None, homeplughandler=None):
self.callbackAddToTrace = callbackAddToTrace
self.callbackShowStatus = callbackShowStatus
@ -400,6 +437,9 @@ class hardwareInterface():
if (getConfigValue("digital_output_device")=="celeron55device"):
self.mainfunction_celeron55device()
if (getConfigValue("digital_output_device")=="mqtt"):
self.mainfunction_mqtt()
if getConfigValueBool("exit_on_session_end"):
# TODO: This is a hack. Do this in fsmPev instead and publish some
# of these values into there if needed.
@ -478,6 +518,35 @@ class hardwareInterface():
self.addToTrace("CHAdeMO: No current limit update for over 1s, setting current to 0")
self.accuMaxCurrent = 0
def mainfunction_mqtt(self):
self.mqttclient.loop(timeout=0.1)
# The callback for when the client receives a CONNACK response from the server.
def mqtt_on_connect(self, client, userdata, flags, rc):
self.addToTrace(f"MQTT connected with result code {rc}")
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe(getConfigValue("mqtt_topic") + "/#")
def mqtt_on_message(self, client, userdata, msg):
if msg.topic == getConfigValue("mqtt_topic") + "/battery_voltage":
self.accuVoltage = float(msg.payload)
self.addToTrace("MQTT: Set battery voltage to %f V" % self.accuVoltage)
elif msg.topic == getConfigValue("mqtt_topic") + "/target_voltage":
self.accuMaxVoltage = float(msg.payload)
self.addToTrace("MQTT: Set target voltage to %f V" % self.accuMaxVoltage)
elif msg.topic == getConfigValue("mqtt_topic") + "/target_current":
self.accuMaxCurrent = float(msg.payload)
self.addToTrace("MQTT: Set current request to %f A" % self.accuMaxCurrent)
elif msg.topic == getConfigValue("mqtt_topic") + "/soc":
self.simulatedSoc = float(msg.payload)
self.soc_percent = self.simulatedSoc
self.addToTrace("MQTT: Set SoC to %f %%" % self.simulatedSoc)
elif msg.topic == getConfigValue("mqtt_topic") + "/inlet_voltage":
self.inletVoltage = float(msg.payload)
self.addToTrace("MQTT: Set inlet voltage to %f V" % self.inletVoltage)
def myPrintfunction(s):
print("myprint " + s)
@ -488,6 +557,8 @@ if __name__ == "__main__":
hw.mainfunction()
if (i==20):
hw.showOnDisplay("Hello", "A DEMO", "321.0V")
hw.setChargerParameters(500, 125)
hw.setChargerVoltageAndCurrent(360, 100)
if (i==50):
hw.setStateC()
if (i==100):
@ -505,6 +576,6 @@ if __name__ == "__main__":
hw.setRelay2Off()
if (i==320):
hw.showOnDisplay("This", "...is...", "DONE :-)")
sleep(0.03)
sleep(0.01)
hw.close()
print("finished.")

26
simulateMqttBackend.py Normal file
View file

@ -0,0 +1,26 @@
import paho.mqtt.client as mqtt
from configmodule import getConfigValue, getConfigValueBool
def mqtt_on_connect(client, userdata, flags, rc):
client.subscribe(getConfigValue("mqtt_topic") + "/#")
def mqtt_on_message(client, userdata, msg):
global simulatedInletVoltage
if msg.topic == getConfigValue("mqtt_topic") + "/fsm_state":
if "CableCheck" in msg.payload.decode("utf-8"):
simulatedInletVoltage = 0
if "PreCharging" in msg.payload.decode("utf-8"):
client.publish(getConfigValue("mqtt_topic") + "/inlet_voltage", simulatedInletVoltage)
simulatedInletVoltage = simulatedInletVoltage + 15
elif msg.topic == getConfigValue("mqtt_topic") + "/pev_voltage":
client.publish(getConfigValue("mqtt_topic") + "/inlet_voltage", msg.payload)
elif msg.topic == getConfigValue("mqtt_topic") + "/pev_current":
client.publish(getConfigValue("mqtt_topic") + "/target_current", msg.payload)
simulatedInletVoltage = 0
mqttclient = mqtt.Client()
mqttclient.on_connect = mqtt_on_connect
mqttclient.on_message = mqtt_on_message
mqttclient.connect(getConfigValue("mqtt_broker"), 1883, 60)
mqttclient.loop_forever()

View file

@ -39,9 +39,11 @@ sysctl net.ipv6.conf.eth0.keep_addr_on_down=1
# Todo: Why this needed? On raspberry, where the NetworkManager is not runnning, this disturbs, because
# afterwards the pyPlc does not see the interfaces IPv6 address.
# Todo: make this configurable, for the cases we need this.
# ip link set eth0 down
ip link set eth0 down
ip link set can0 down
sleep 1
# ip link set eth0 up
ip link set eth0 up
ip link set can0 up type can restart-ms 100 bitrate 500000
sleep 1
# show the addresses
@ -53,17 +55,25 @@ pwd
# create directory for the log files.
mkdir -p log
# prepare the file names for the log files
pushd log
gzip *.log || true
popd
date=$(date "+%Y-%m-%d_%H%M%S")
logfile=./log/"$date"_pevNoGui.log
tcpdump_logfile=./log/"$date"_tcpdump.pcap
#logfile=./log/"$date"_pevNoGui.log
#logfile=./log/pevNoGui.log
index=`cat log/logindex`
logfile=`printf "log/%04d_pevNoGui.log" $index`
tcpdump_logfile=`printf "log/%04d_tcpdump.pcap" $index`
index=$(($index + 1))
echo $index > log/logindex
echo "logfile: $logfile"
echo "tcpdump_logfile: $tcpdump_logfile"
# start the tcpdump
start_tcpdump "$tcpdump_logfile"
#start_tcpdump "$tcpdump_logfile"
echo "$date" >> "$logfile"
echo "$date" > "$logfile"
git log --oneline -1 >> "$logfile" || echo "Not a git repo" >> "$logfile"
ip addr >> "$logfile"
pwd >> "$logfile"