question

andy156 avatar image
andy156 asked

Roll your own dynamic ESS

For those wanting to roll your own. Here are parts of what I use. I can't just upload my project as there are too many sensitive links/passwords for me to easily sanitise it. However it will give a starting point.

I have an Economy 10 tariff so there are 3 cheap and 3 expensive periods in the day. expensive 0030-0430,0730-1330,1630-2030. The aim is to fill the batteries with enough solar/cheap electricity to bridge the expensive periods. The usage is calculated based on the solar forcast, the temperature forecast (electricity usage for heat pump) and a baseline usage. Minimising charge rates and other little things are done for efficiencies which probably add to a few £100 in savings a year.

figure 1.

screenshot-2023-10-21-at-155722.png

Setting up cheap/exppensive periods. I am on E10. Also, SOC during cheap periods can be overridden.

figure 2.


screenshot-2023-10-21-at-155730.png

the main loop which takes inputs from the system and decides how to control the quattro based on charging/discharging/holding charge to achieve a calculated SOC. After the master loop certain things can be altered to allow charging Eddie and Zappie.

figure 3.

screenshot-2023-10-21-at-155738.png

Determining the SOC to charge to based on the forecasts.

MultiPlus Quattro Inverter Charger
2 |3000

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

6 Answers
andy156 avatar image
andy156 answered ·

Master loop code in figure 2

/*
IMPROVEMENTS:
1. Look ahead to see what predicted solar is. If it is going to be a lot later then consider using the power available in the battery

2. Change the charge rate to be based on a fixed time period from when the SOC last changed to avoid the step in charge rate

3. Ensure the battery is fully charged every few days

4. Speed up the control loop using MQTT

5. If using 1000, then look to delay so that it charges at the end of the period

6. Look to add 4kWh for frost forecast

7. Add logic so that batteries dont sit at 100% all day

8. Dump extra into car each night during the summer months
*/


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={};

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);  //if after midnight sets to today

    var timeR = currentSchedule["length"].split(":");
    var diff = timeR[0] * 60 + timeR[1] * 1;

    var endTime = new Date(startTime.valueOf() + diff * 60000);
// endtime-nowtime>24h

    // //Correct for times running over midnight
    // if (endTime.valueOf() < now.valueOf()) {
    if (endTime.valueOf() - now.valueOf() > 86400000){
        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*.8
    }

    // 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  // Don't allow power in from the grid. uSe the battery.
        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
    //    node.warn(chargerOn);
       // 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 = clampValue(setPointToMeetBattery(0), 0, maxPowerHouseIn) +100
        }
        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  //cap at 100 %
        
        // nextSOC=SOCInfo.notBelowDuringCharge

        if (currentSchedule.override){
            nextSOC=currentSchedule.SOC
            nodeWarn="blue"
        }
        
        const minimumChargeRate = 1000
            
        const now = new Date();
        var chargeRate = getChargeRateToMeetSOC(nextSOC)

        // change timesocchanged if no tick in expected period. If we aren't charging as quickly as expected then give it a little boost
        var lastTick=flow.get("timeSOCChanged")
        var timePeriodForPercent=384/chargeRate * (60000*60)
        if (now.valueOf()-lastTick>timePeriodForPercent){
            flow.set("timeSOCChanged",now.valueOf())
        }

        /* 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
            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
        } else {
            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
        }
        
        // if (SOC>90) {
        //     chargeRate=chargeRate+400
        // }
        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

// node.warn(state.name);
// node.warn(newSetpoint)
newSetpoint = Math.round(newSetpoint);
// node.warn(newSetpoint)
msg.debug = debug;

// msg2.payload = currentSchedule["SOC"];

/*Value types

0 - Charge allowed
1 - Charge disabled
*/
log("chargeron:"+chargerOn)
var dvcc=-1
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:newSetpoint,
    essChargeFlag: chargerOn ? 0 : 1,
    maxInverterPower:maxInverterPower,
    dvcc:dvcc,
    chargeState:state.name
}

// 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.

andy156 avatar image
andy156 answered ·

figure 3. Calculate SOC to target in powerusage2

/*
TODO

calculate minimum daily consumption.. ie don't charge up too much when it isn't necessary
calculate estimated charge for 0730 in the morning so we can charge
the car.

*/


//sunrise
//stop time
//msg.forecast [ time:dt:temp:heatingLoad:baseLoad:] 30 minute segments
//global.pvobject.forecasts[pv_estimate:period_end:]

const minSOCOnBattery=20
const isToday = (someDate) => {
    const today = new Date()
    return someDate.getDate() == today.getDate() &&
        someDate.getMonth() == today.getMonth() &&
        someDate.getFullYear() == today.getFullYear()
}


var pv=global.get("pvobject").forecasts

//remove all the out of date objects
var now=new Date( Date.now())


// now.setMinutes(30-now.getMinutes())
const nowM=now.getMinutes()
now.setMinutes(nowM + (30 - nowM % 30))
now.setSeconds(0)
now.setMilliseconds(0)

var nowV=now.valueOf()

pv=pv.map(value=>{
    var time=new Date(value.period_end)
    var dt = time.valueOf()
    return {
        pv:value.pv_estimate,
        time:value.period_end,
        dt:dt
    }
})

pv=pv.filter(n=>n)

const tdt=nowV

var forecast=msg.forecast
var length=Math.min(forecast.length,pv.length)

// node.warn(length)
forecast.length=length
pv.length=length
// node.warn(now);
var forecast = msg.forecast.filter(value=>{
    return value.dt>=nowV
})

// node.warn(forecast)
const SOC = global.get("SOC")
// const SOC = 0

//merge together the two objects
var result =forecast.map(value=>{
        var result = value
        var pvItem = pv.find(value2=>{
            // node.warn(value.dt+" "+ value2.dt)
            return value.dt==value2.dt}
            )
            
        if (pvItem)
            result.pv=pvItem.pv*1000
         else 
            result.pv=0
            
        result.expectedPowerUsage=result.baseLoad+result.heatingLoad-result.pv
        result.load=result.baseLoad+result.heatingLoad

        result.SOC = SOC // Forward SOC
        return result
    }
)


// Calculate the virtual battery going forward
// This is based on the SOC a the start.... should this be the next period SOC?
const totalBattery= 4800*8
// Work backwards for min SOC to hit in each expensive period
result = result.reduceRight((accum, value) => {

    var SOCUsed = 0
    var resultArray = accum.arr
    // node.warn(accum)
    var runningTotalInCurrentPeriod = accum.runningTotalInCurrentPeriod
    var runningTotalForAllPeriods = accum.runningTotalForAllPeriods
    // node.warn(runningTotalForAllPeriods)
    SOCUsed = value.expectedPowerUsage * 50 / totalBattery
    var before=runningTotalForAllPeriods
    if (!value.isCheap) {
        if (resultArray.length == 0 || resultArray.slice(-1)[0].isCheap)
            runningTotalInCurrentPeriod = 0

        // SOCUsed = value.expectedPowerUsage * 50 / totalBattery  //isnt including stuff at the end of the period in the morning
        runningTotalInCurrentPeriod = max(0,runningTotalInCurrentPeriod + SOCUsed)
        runningTotalForAllPeriods = Math.min(80, max(0, runningTotalForAllPeriods + SOCUsed))
        // Math.min(80, max(0, runningTotalForAllPeriods + SOCUsed))
    } else {

        // Set to the last if in a cheap period. ie the value we need to charge to
        if(resultArray.length>0)
            runningTotalInCurrentPeriod = resultArray.slice(-1)[0].minSOCInPeriod

        if (SOCUsed<0)  {
            runningTotalForAllPeriods = max(0, runningTotalForAllPeriods + SOCUsed)  //Take into account afternoon generation in the cheap period.
        }
    }
    var wt = value.isCheap ? "Cheap" : "expensive"
    // node.warn(wt+runningTotalForAllPeriods)

    value.minSOCInPeriod = runningTotalInCurrentPeriod
    var after = runningTotalForAllPeriods
    value.minSOCToMeetFuturePeriods = runningTotalForAllPeriods
    // node.warn("soc"+SOCUsed+"before:"+before+"after"+after)
    resultArray.push(value)
    result = {
        arr: resultArray,
        runningTotalInCurrentPeriod: runningTotalInCurrentPeriod,
        runningTotalForAllPeriods: runningTotalForAllPeriods
    }
    return result
}, {
    arr: [],
    runningTotalInCurrentPeriod: 0,
    runningTotalForAllPeriods: 0
})// accum: result,
// Working backwards gives the total required for the whole period.... but what about the other way?
result = result.arr.reverse()


result = result.reduce((result,value)=>{
    //initial values
    var lastSOC=SOC
    var lastSOC0=0
    var lastcappedSOC=SOC // This value is capped at 100% whereas the other isn't giving an idea of lost power
    var lastItem
    if (result.length>0) {
         lastItem = {...result.slice(-1)}
        lastSOC=lastItem[0]["SOC"]
        lastcappedSOC = lastItem[0]["cappedSOC"]
        lastSOC0 = lastItem[0]["SOC0"]
    } 
    const change = value.expectedPowerUsage*50 / totalBattery   //.5*100 to get %
    value.SOC=lastSOC-change
    value.cappedSOC=lastcappedSOC-change
    value.SOC0=lastSOC0-change
    if (value.SOC0<20)
        value.SOC0=20
    if (value.SOC0>100)
        value.SOC0=100

    if (value.SOC<minSOCOnBattery)
        value.SOC=minSOCOnBattery
    if(value.cappedSOC>100)
        value.cappedSOC=100
    if (value.cappedSOC<=minSOCOnBattery)
        value.cappedSOC=minSOCOnBattery
    result.push({...value})
    
    return result
},[])

// Maximum SOC before midnight
const maxSOCToday=result.filter(value=>{
    return isToday(new Date(value.dt))
}).reduce(
    (accum,value)=>{
        if (value.SOC>accum)
            accum=value.SOC
        return accum
    }
,0)

const spareSolarToday = result.filter(value => {
    return isToday(new Date(value.dt))
}).reduce(
    (accum, value) => {
        if (value.SOC>=100 && value.expectedPowerUsage <0)
            accum = accum-value.expectedPowerUsage
        return accum
    }
    , 0)/2.0

const maxCappedSOC = result.filter(value => {
    return isToday(new Date(value.dt))
}).reduce(
    (accum, value) => {
        if (value.cappedSOC > accum)
            accum = value.cappedSOC
        return accum
    }
    , 0)

const minSOC = result.reduce(
    (accum, value) => {
        if (value.SOC < accum)
            accum = value.cappedSOC
        return accum
    }
    , 100)


// what is the maximum in the next 24 hrs
const maxSOCForCorrection = result.filter(value=>{
    return value.dt<(now.valueOf()+60*60824*1000)   // Values in next 24 hours
}).reduce(
    (accum, value) => {
        // node.warn(value.time);
        if (value.SOC0 > accum)
            accum = value.SOC0
        return accum
    }
    , 0)

const maxSOC0 = result.filter(value => {
    return value.dt < (now.valueOf() + 60 * 60*24 * 1000)   // Values in next 24 hours
}).reduce(
    (accum, value) => {
        // node.warn(value.time);
        if (value.SOC0 > accum)
            accum = value.SOC0
        return accum
    }
    , 0)
const minSOC0 = result.reduce(
    (accum, value) => {
        if (value.SOC0 < accum)
            accum = value.SOC0
        return accum
    }
    , 100)

function max(a,b){
    if (a>b)
    return a
    else
    return b
}

var SOCInfo2 = flow.get("SOCInfo")

SOCInfo2["correction"] = 0
// look at the max SOC in the next 24 hours.  This doesn't work when SOC is high now and only decreasing.
//. e.g. 2130 soc is 70 and tomorrow remaining at 20 all day.  This should really be a case for not
// discharging as we will only need to charge agaain tomorrow

// if (maxSOCForCorrection < 50) {
    
//     var correction =(100 - maxSOCForCorrection-20)/2
//     SOCInfo2["correction"] = correction
//     result.map(value => {
//         value.minSOCInPeriod =(Math.ceil(value.minSOCInPeriod) +value.minSOCToMeetFuturePeriods)/2
//     })
// }
var spare = Math.max(0, (100 - maxSOC0) - 20)
//Correct the minsoc in period
result = result.map(value => {
    const before = value.minSOCInPeriod
    value.minSOCInPeriodO=value.minSOCInPeriod

    // minsoc to meet future periods... always this since we don't need to be operating at minimum
    // 
    // value.minSOCInPeriod = (Math.ceil( value.minSOCToMeetFuturePeriods) / 2 + spare

    // we want a minimum of value to meet. if the spare is more than we can start with this in the battery as well.
    value.CalculatedSOCForPeriod = Math.max(Math.ceil(value.minSOCToMeetFuturePeriods),spare)
    value.spare=spare
    // node.warn(value);
// node.warn("Before"+before+" after:"+value.minSOCInPeriod+"minsoctomeet"+value.minSOCToMeetFuturePeriods+"spare"+spare+"soc0"+maxSOC0)
    return value
})

flow.set("SOCInfo", SOCInfo2)
msg.maxSOCToday = maxSOCToday
msg.maxCappedSOC = maxCappedSOC

now=new Date()

if (now.getHours()<5)
    flow.set("earlyForecast",result)
    
msg.payload={
    forecast:result,
    maxSOCToday:maxSOCToday,
    maxCappedSOC:maxCappedSOC,
    minSOC:minSOC,
    minSOC0:minSOC0,
    maxSOC0:maxSOC0,
    spareSolarToday:spareSolarToday
}
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.

andy156 avatar image
andy156 answered ·

Usage is also logged to allow graphing such as the following in influx and grafana. This took ages to get right and the code required to generate the graph is produced in the following block.

screenshot-2023-10-21-at-161105.png

function log(message) {
    if (flow.get("smaFlag")) {
        node.warn(message)
    }
}
var payload =
{
    "solaredgepower": 5004,
    "BatteryPower": 6064,  //- discharge, positive charge
    "GridIn": 20,
    "acconsumption": 6310,
    "accoupled 1": 5004,
    "mpptspower": 7448.75,
    "vebus_power": -1286, //inverter -> acout/grid
    "dc_system": null
}

payload = msg.payload
payload.mpptspower = payload.mpptspowerL + payload.mpptspowerR


//Info i want
// Consumption = Grid + Battery + Solar
// Solar -> Feed+Battery+Grid
var solar = {
    "SolarToGrid": 0,
    "SolarToBattery": 0,
    "SolarToLoad": 0
}
var consumption = {
    "GridToBattery": 0,
    "GridToLoad": 0,
    "SolarToLoad": 0,
    "BatteryToLoad": 0
}

// grid = battery - solar + load
const inverter = {
    "inverter": payload.vebus_power,
    dischargingSolar: 0,
    dischargingBattery: 0,
    chargingFromGrid: 0,
    chargingFromSolar: 0
}

var pvSE= flow.get("solaredgepower")
var opvSE = flow.get("Osolaredgepower")

if (pvSE>5000 && opvSE)
    pvSE = opvSE
if (pvSE==0 && opvSE>100)
    pvSE=opvSE
flow.set("solaredgepower",0)
flow.set("Osolaredgepower",pvSE)

const input = {
    mpptpv: payload.mpptspowerL + payload.mpptspowerR,
    pvSE: pvSE,
    pv: pvSE + payload.mpptspower,
    battery: payload.BatteryPower,
    load: payload.acconsumption,
    grid: payload.GridIn,
    SOC: payload.SOC,
    inverter: inverter.inverter
}

/* 
solar = battery-grid+load
solar to battery = solar +grid-load
solar to grid = battery+load-solar
solar to load = solar -battery-load
*/
var output = {
    B2L: 0,
    B2G: 0,
    S2L: 0,
    S2G: 0,
    S2B: 0,
    S2I: 0,
    B2I: 0,
    G2L: 0,
    G2I: 0,
    G2B: 0
}
var B2L = 0
var B2G = 0
var S2L = 0
var S2G = 0
var S2B = 0
var S2I = 0
var B2I = 0
var G2L = 0
var G2I = 0
var G2B = 0
var sectionText = ""
log("start sma")

function calculateSMAValues(input) {
 
    const battery = input.battery
    var inverter = input.inverter
    const grid = input.grid
    const load = input.load
    const mpptpv = input.mpptpv
    const pvSE = input.pvSE
    const inverterBacking=mpptpv-battery
    // Sanity check on inverter...
    // if (inverter<0 && -inverter>inverterBacking+1000){
    //     inverter=inverter/inverter*inverterBacking
    // }

    if (inverter < 0) { // discharging
        sectionText = "inverter"
        B2I = (battery >= 0) ? 0.0 : (-battery)
        S2I = (battery >= 0) ? mpptpv - battery : mpptpv // If battery is charging then it is coming from mppt
        S2B = (battery > 0) ? battery : 0.0
        const ratio = S2I / (B2I + S2I)


        S2I = inverter * ratio  // If S2I -1000
        B2I = inverter - S2I //-1000--1000

        log("ratio :" + ratio)
        if (ratio > 1 || S2I > 16000) {
            var tmp = "ratio>1:" + ratio + "B2I" + B2I + "S2I" + S2I
            log(tmp)
            global.set("debug", tmp)

        }
        // log("B2i: "+B2I)
        //correct for losses through inverter
        // var ratio=-inverter/(S2I+B2I)
        // S2I=S2I
        // B2I = B2I 
        // S2B=S2B

        // log("grid:"+grid)

        if (grid == 0) {
            sectionText = sectionText + " da"
            log("da")
            S2L = pvSE - S2I
            B2L = -B2I
        } else if (grid > 0) {// grid coming in
            sectionText = sectionText + " db"
            log("db")
            log("pvse:" + pvSE + " s2i:" + S2I + " load:" + load + " grid:" + grid + " B2I:" + B2I)

            G2L = grid
            S2L = pvSE - S2I
            B2L = -B2I //load - grid - S2L //S2I+pvSE
            B2L = B2L < 0 ? 0 : B2L
            log("S2B:" + S2B + " G2L:" + G2L + " S2L:" + S2L + " b2l:" + B2L)
            //Sanity check on inverter. If it is greater than sum of solar and battery then reduce it.


            if (S2B > 0 && G2L > 0) {
                if (S2B > G2L) {
                    sectionText = sectionText + " s2b>g2l"
                    log("s2b>g2l")
                    S2B = S2B - G2L
                    S2L = S2L + G2L
                    G2B = G2L
                    G2L = 0
                } else { //S2B<G2L
                    sectionText = sectionText + " s2b<g2l"
                    log("s2b<g2L")
                    G2B = S2B
                    S2L = S2L + S2B
                    G2L = G2L - S2B
                    S2B = 0
                }
            }
        } else if (grid < 0) { // grid going out + discharging
            log("pvse:" + pvSE + " s2i:" + S2I + " load:" + load + " grid:" + grid + " B2I:" + B2I)
            if ((pvSE - S2I) <= load) {
                sectionText = sectionText + " dc"
                // (load = S2L+B2L)
                S2L = pvSE - S2I
                B2L = load - S2L
                B2G = -grid
            }
            else if ((pvSE - S2I) > load) {
                sectionText = sectionText + " dd"
                log("dd")
                S2L = load
                // S2G=pvSE+S2I-load //Causes fluctiations when sunny
                // pvSE + S2I

                S2G = pvSE - S2I - load
                if (S2G>-grid)
                    S2G = -grid

                B2G = -grid - S2G
                log("B2G" + B2G)
            }
        }
    } else if (inverter == 0) {
        if (pvSE >= load) {
            sectionText = sectionText + " ia"
            log("ia")
            S2L = load
            S2G = grid
        } else {
            sectionText = sectionText + " ib"
            log("ib")
            S2L = pvSE
            G2L = grid
        }
        S2B = mpptpv
    } else if (inverter > 0) //charging
    {
        if (pvSE >= load) {
            S2L = load
            if (grid > 0) { //Incoming grid
                sectionText = sectionText + " ca"
                log("ca")
                G2I = grid
                S2I = pvSE - load
            } else { //Exporting
                sectionText = sectionText + " cb"
                log("cb")
                S2G = -grid
                S2I = -grid - load + pvSE //grid-load+pvSE = inverter-pvSe+load
            }

            //s2l = 5001

        } else if (pvSE < load) {
            S2L = pvSE
            if (grid >= 0) {   //grid coming in.
                sectionText = sectionText + " cc"
                log("cc")
                G2L = load - pvSE
                G2I = grid - G2L
            }
        }

        const ratio = inverter / (G2I + S2I)
        G2I = G2I * ratio
        S2I = S2I * ratio
        // charging so whole load will be grid+S2L at this point
        var diff = load-S2L // guardanteed to be positive
        if (mpptpv+S2I>=diff) {
            S2L=load
            S2B=mpptpv+S2I-diff
            G2L=G2L-diff
            G2B=G2I+diff
        } else {
            S2L=S2L+mpptpv+S2I
            S2B=0
            G2L=G2L-mpptpv-S2I
            G2B=G2I+mpptpv+S2I
        }
        
        // Need S2L to be as big as possible. currently G2b is first.

    }
    output = {
        section: sectionText,
        B2L: B2L,
        B2G: B2G,
        S2L: S2L,
        S2G: S2G,
        S2B: S2B,
        S2I: S2I,
        B2I: B2I,
        G2L: G2L,
        G2I: G2I,
        G2B: G2B,
        load: load

    }

    return output

}

output = calculateSMAValues(input)

var error = false
var errorText=""
//Check for errors:
if (output.S2L + output.B2L + output.G2L != output.load) {
    error = true
    errorText = "load inputs greater than load"
}
if ((pvSE+input.mpptpv)> 16500 || output.S2B+output.S2G+output.S2L>16500){
    error=true 
    errorText="Excessive PV production"
}


if (error){
    // node.warn("Error")
    var errorsArray = flow.get("errors")
    const time = new Date(Date.now())
    const result = {
                error: errorText,
                time:time.toLocaleTimeString(), 
                input: input, 
                output: output
                }

    // node.warn(errorsArray)
    // node.warn(result)
    if (!errorsArray)
        errorsArray = [result]
    else
        errorsArray.push(result)
    
    flow.set("errors", errorsArray)
}
// zeros and nulls zero
function nz(value, name) {
    if (value < 0)
        log("WARNING negative value for. " + name + " " + value)
    value= value < 0 ? 0 : value
    // if (value == 0)
    //     value=null
    return value
}

S2G = nz(output.S2G, "S2G")
S2B = nz(output.S2B, "S2B")
S2B = nz(output.S2B, "S2B")
S2L = nz(output.S2L, "S2L")
G2L = nz(output.G2L, "G2L")
B2L = nz(output.B2L, "B2L")
B2G = nz(output.B2G, "B2G")
G2B = nz(output.G2B, "G2B")

B2G=B2G<50?0:B2G

var load = nz(output.load)

var result = {
    solar2Grid: S2G,
    solar2Battery: S2B,
    solar2Load: S2L,
    battery2Load: B2L,
    "grid2Load": G2L,
    grid2Battery: G2B,
    battery2Grid: B2G,
    SOC : input.SOC,
    totalLoad: load
}
log ("sectiontext:"+sectionText)
msg.payload.pvse = pvSE
msg.payload.sectionText = sectionText
msg.powerRouting = result
msg.payload2 = msg.payload   // The raw values

msg.payload = [result, {
    device: "cerbo"
}];
msg.measurement = "SMAView";

log("end sma")
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.

andy156 avatar image
andy156 answered ·

If you have a lot of solar panels and a large inverter you might want to protect things in your house in the UK. The limit these days seems to be 266V and a lot of things will die at this voltage. I dynamically limit the export with the figure 2 limit export node


/*
battery:,
SOC:msg.payload["BatterySOC"],
batteryPower:msg.payload["BatteryPower"],
setpoint :msg.payload["ESS_Setpoint"],
mppt : mppt,
voltage :msg.payload["BatteryVoltage"],
globalState : global.get("state"),
PV_Power : (pvse + mppt),
consumption : msg.payload["consumption"],
rrentSchedule : global.get("CurrentSchedule"),
SOCInfo : flow.get("SOCInfo"),
grid : msg.payload["grid"],
voltageIn : msg.payload["voltageIn"]
}
*/
var maxV=251
var state = msg.payload
let pvse=state["PV_SE"]
let consumption=state["consumption"]
// This line ensures that we don't go over 253 volts.
var feedIn = context.get("feedIn")
var vDiff = maxV - state.voltageIn
var gridDiff = feedIn + state.grid
var correction = 0
const ovdiff=context.get("vdiff")

if (vDiff < 0) {
    // Greater than the voltage we want. make a big correction
    if (ovdiff>=0) {
        feedIn = feedIn-200
    } else {
        feedIn = feedIn + vDiff * 400
    }
} else if (vDiff > .55 && gridDiff < 500) {
    // Creep up on the export figure from below. Can go faster at the start.
    correction = (30 * Math.pow(vDiff, 2))
    feedIn = feedIn + correction
}
context.set("vdiff",vDiff)
// node.warn(feedIn + ":" + vDiff + ":" + gridDiff + "c:" + correction)
feedIn = Math.round(clampValue(feedIn, 0, 13000))

// always feed in a trickle from the inverter so that we don't over charge battery
// feedIn = Math.max(Math.max(pvse-consumption,0)+250,500)

context.set("feedIn", feedIn)
msg.payload = feedIn


node.status({ fill: "green", shape: "dot", text: feedIn +"kW"});

return msg;


function clampValue(value, min, max) {

    if (value < min)
        value = min
    else if (value > max)
        value = max

    return value
}
2 |3000

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

kevgermany avatar image
kevgermany answered ·

@Andy156

Looks good, but I didn't bother to read the code.

Moving to modifications as it's Node Red.

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.

andy156 avatar image andy156 commented ·
No problem. Thanks. Just posted as someone wanted it. Might help someone get started :P I'd probably suggest that most should use the built in. This is handy if extra flexibility is required.
0 Likes 0 ·
wkirby avatar image
wkirby answered ·

This is really nice to see.
I have a much smaller flow which helps me to optimise Octopus Go (4 hours of sensibly priced electricity). It sits idle for at least six months of the year but it's started working again now that the days are getting shorter and more rubbish.

2 |3000

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