question

michael-protogerakis avatar image
michael-protogerakis asked

Controlling single phase ESS via nodered

I have set up a single phase ESS (Multiplus2) which is working as expected so far.

I would like to control the charging from the grid flexibly via nodered while relying on the gx ess functionality to provide zero feed-in functionality.

I found that I can control "disable charging" via nodered but that works only during the scheduled charging times. And while a charge is scheduled zero feed-in functionality is not working although charging is disabled.

What is the best way to achieve charging control via nodered while in ess mode?

ESSNode-RED
2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

2 Answers
hominidae avatar image
hominidae answered ·

...you can modify DVCC "Charge Current" and DVCC "Battery Charge Voltage"....in ESS you can modify "inverter Power" for discharging.

Also, in ESS mode, you can use the grid-setpoint to control charge/discharge "direction" and Power

1 comment
2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

michael-protogerakis avatar image michael-protogerakis commented ·

thank you. that works for me.

0 Likes 0 ·
andy156 avatar image
andy156 answered ·

I had the same problem and don't use the scheduled charges at all. I just use the ESS and piggy back the charging functionality based on my own scheduling in node-red. There is then a dashboard to help control it all.


Here is my code which will give you starter for 10. A dictionary with values is passed in having received them from wherever (nodes or mqtt) for example.


I then have a state based on time and other things. I have economy 10 so three cheap and three expensive periods. The aim is to minimise grid use and make sure that the expensive periods are covered. The outputs are then forked into the respective victron nodes. There is some historic garbage I haven't clean up yet. However it will give an idea of what is required. A lot of other calculations are performed elsewhere ie downloading weather/solar forecast and working out SOC for next 2 days to determine if the battery is discharged or not.


const maxPowerHouseIn=230*60
function log(text) {
    // text = text
    // node.warn(text)
}
function efficiencyAt(power) {
    power = power / 1000
    const a = (power * .1) / 6    
    const eff = 0.9366 - a   //.966 was the original number
    return eff
}
class ChargeState {
    static HoldCharge = new ChargeState('HoldCharge');
    static UseBattery = new ChargeState('UseBattery');
    static EmptyBattey = new ChargeState('EmptyBattery')
    static ExpensivePeriod = new ChargeState('ExpensivePeriod')
    static ChargeBattery = new ChargeState('ChargeBattery');
    constructor(name) {
        this.name = name;
    }
    toString() {
        return `Color.${this.name}`;
    }
}
var debug={};
var msg2 = {};   //esschargeflag
var msg3={};    //inverter max
var msg4 = {payload:-1}; //dvcc
const values=msg.values
const battery = values.battery;
const SOC = values.SOC;
const batteryPower = values.batteryPower;
const setpoint = values.setpoint;
const mppt = values.mppt;
const DCvoltage = values.voltage
const globalState= global.get("state")
// const PV_Power = msg.payload["PV_SE"] + mppt;
const PV_Power = values.PV_Power;
const consumption = values.consumption;
var currentSchedule = global.get("CurrentSchedule");
const SOCInfo = values.SOCInfo
const grid = values.grid
const voltageIn = values.voltageIn


var maxInverterPower = 0;
var newSetpoint = 20;
var nodeWarn="green";
debug["Default"]={"battery":battery,"SOC":SOC,"setpoint":setpoint};
function getChargeStateInCheapPeriod() {
    var state =0
    
    var desiredSOC
    // const notBelowSOC = SOCInfo.notBelowDuringCharge
    const notBelowSOC = SOCInfo.nextSOC
    if (currentSchedule.override)
        desiredSOC=currentSchedule.SOC
    else 
    {
        desiredSOC = notBelowSOC
        desiredSOC = desiredSOC > 100 ? 100 : desiredSOC
    }
        // desiredSOC = SOCInfo.nextSOC // THis is enough to get through the period
    
    log("SOC:"+SOC+"notbel"+notBelowSOC+"desired"+desiredSOC)
    // if(SOCInfo.maxSOCToday==100)
    // {
    //     state = ChargeState.EmptyBattey
    // }else
    // This needs a lot of tweaking... look ahead to see if any charge needed and also if not then dont use battery
     if ((SOC > notBelowSOC || SOCInfo.maxSOCToday==100) && SOC>desiredSOC && SOCInfo.correction==0)
        state = ChargeState.UseBattery
    else if (SOC >= desiredSOC)
        state = ChargeState.HoldCharge
    else
        state = ChargeState.ChargeBattery


    return state
}
//New charge state c
var state = ChargeState.ChargeBattery
if (currentSchedule.cheapPeriod){
    state=getChargeStateInCheapPeriod();
} else {
    state= ChargeState.ExpensivePeriod
}
// This will aim to hit the SOC at the end of the period
function getChargeRateToMeetSOC(desiredSOC){
    // Do the math to find the new charge rate
    var now = new Date();
    var hoursNow = now.getHours();
    var minutesNow = now.getMinutes();
    var startTimeStr = currentSchedule["start"];
    var startT = startTimeStr.split(":");
    var startHour = startT[0];
    var startMinute = startT[1];
    var startTime = new Date();
    startTime.setHours(startHour)
    startTime.setMinutes(startMinute);
    var timeR = currentSchedule["length"].split(":");
    var diff = timeR[0] * 60 + timeR[1] * 1;
    var endTime = new Date(startTime.valueOf() + diff * 60000);
    //Correct for times running over midnight
    if (endTime.valueOf() < now.valueOf()) {
        var etv = endTime.valueOf() - 24 * 60 * 60 * 1000;
        endTime = new Date(etv);
    }
//  
    // var remainingTime = (endTime.valueOf() - now.valueOf()) / 60000;
    var SOCChangedTime=flow.get("timeSOCChanged")
    var remainingTime = (endTime.valueOf() - SOCChangedTime) / 60000;
    
    var remainingSOC = desiredSOC - SOC;
    var powerRemaining = remainingSOC / 100 * 4800 * 8;
    
    var powerPerHour = Math.round(powerRemaining / remainingTime * 60);
    
    // How much needs to go into the battery.
    return powerPerHour
}
// Meets the battery charge discharge based on grid only.
function setPointToMeetBattery(chargeRate){
// node.warn(PV_Power)
    if (chargeRate>0) {
        chargeRate = chargeRate / efficiencyAt(chargeRate)
    } else {
        chargeRate = chargeRate / efficiencyAt(chargeRate)
    }
    // If this isn't added then the battery will discharge at about 100W
    // This currently causes oscillations.. needs damping
    if (chargeRate==0){
        const battery = global.get("state_Battery")
        chargeRate=-battery.power*.7
    }
    // new set point
    // Problem is the mppt... In the morning it is the only power and causes issues
    const setPoint= consumption - PV_Power + chargeRate;
    return setPoint
}
function clampValue(value,min,max){
   
    if (value <min)
        value = min
    else if (value>max)
        value = max
    return value
}
var powerPerHour=0
var chargerOn=false
function isSunny(){
    return msg.payload.PV_State != 0
}
log("state"+state.name)
//If we are not in a charge schedule then return 20 which will be the default state to use the battery
switch (state.name) {
    case ChargeState.ExpensivePeriod.name:
    {
        log ("expensive charge: "+0)
        if (isSunny())
            chargerOn = true  // This is required if pv is greater than load
         else 
            chargerOn= false // It is dark so we won't be charging
         
        maxInverterPower = -1
        newSetpoint = 0
        log("max inverter"+ maxInverterPower)
        break
    }
    case ChargeState.EmptyBattey.name:
    {
        maxInverterPower=10000
        // var chargeRate= getChargeRateToMeetSOC(SOCInfo.nextSOC)
        newSetpoint=setPointToMeetBattery(-maxInverterPower)
        chargerOn=false
        break
    }
    case ChargeState.UseBattery.name:
    {
            log("usebattery"+ 0)
        if (isSunny())
        {
            maxInverterPower = -1 // Without this the load won't be powered by mppt
            chargerOn = true // The battery won't be powered by excess sunlight without this.
        }else
        {
            var chargeRate = getChargeRateToMeetSOC(SOCInfo.notBelowDuringCharge)
            maxInverterPower = -1 //clampValue(chargeRate,  1000, 9000) // Limit lower level to 1000 since it gets very inefficient below this level.
            chargerOn=false
        }
       
        newSetpoint = 0
        break
    }
    case ChargeState.HoldCharge.name:
    {
        chargerOn = msg.payload.PV_State > 0  // Charge if there is PV
       
       // We have a minimum value of 0 so that we are not exporting. This doesn't work. 
       if(msg.payload.PV_State!=0 && mppt>500)  //if day
            maxInverterPower=-1  // Do this or we won't export any extra power.
        else
            maxInverterPower=0 // Night time freeze the inverter
        
        if (SOC>98)
        newSetpoint=0
        else {
            //was clamped to 9000 for inverter. this is worng
            newSetpoint=clampValue(setPointToMeetBattery(0),0,maxPowerHouseIn) 
        }
        break
    }
    case ChargeState.ChargeBattery.name:
    {
        //need to implemnt hold charge at zero until minimum time to achive 
        chargerOn=true
        maxInverterPower = -1 // depends on sunshine and loads
        var nextSOC=SOCInfo.nextSOC>100?100:SOCInfo.nextSOC
        
        // nextSOC=SOCInfo.notBelowDuringCharge
        if (currentSchedule.override){
            nextSOC=currentSchedule.SOC
            nodeWarn="blue"
        }
        
        const minimumChargeRate = 1000
        var chargeRate = getChargeRateToMeetSOC(nextSOC)
        /* Do nothing until charge rate is 1000. 
        ie leave as late as possible to allow solar to charge.
        If there is DC solar then use that as it is more efficient to direct charge with it
        then power a load.
        */
        if (chargeRate<minimumChargeRate) {
            //reset the time
            const now = new Date();
            flow.set("timeSOCChanged", now.valueOf())
            // chargeRate=Math.min(mppt,chargeRate) //More efficient to charge with the mppt
            newSetpoint=0 // this would not work as we would be using the battery
            chargeRate=0 // keep at zero until minimum charge rate is required to hit the charge rate
        } 
      
        chargeRate = clampValue(chargeRate, minimumChargeRate, 7300) // Limit lower level to 1000 since it gets very inefficient below this level.
        powerPerHour = chargeRate // set this value for the node status
        newSetpoint=clampValue(setPointToMeetBattery(chargeRate),0,12000) // Be nice to the house input and make sure we don't export
        
        break
    }
}
//fixes the battery voltage so that power is diverted. Leaving the battery at the voltage the zappie charging was started.
        // var correction=0
        // if (batteryPower>0) // Charging
        //     correction =  batteryPower/10000+.05
        // else if (batteryPower<0) { 
        //     correction = batteryPower/10000-.04
        // }
        // if (grid<0){
        //     correction = grid/3000
        // }
        // msg4.payload=voltage-correction


newSetpoint = Math.round(newSetpoint);
msg.debug = debug;
msg.payload = newSetpoint;
// msg2.payload = currentSchedule["SOC"];
/*Value types
0 - Charge allowed
1 - Charge disabled
*/
log("chargeron:"+chargerOn)
msg2.payload = chargerOn ? 0:1
// msg2.payload=0
msg3.payload = maxInverterPower
log("max inverter"+maxInverterPower)
const text = state.name + " SOC:" + SOCInfo.nextSOC +  "PowerRq: "+powerPerHour+"W,Set: "+newSetpoint
node.status({fill:nodeWarn,shape:"dot",text: text});
msg.payload={
    setpoint:msg.payload,
    essChargeFlag:msg2.payload,
    maxInverterPower:msg3.payload,
    dvcc:msg4.payload
}
// For ess
// set point, min soc, inverter level,feedin.
return msg
2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.