# question

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.

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

figure 2.

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.

Determining the SOC to charge to based on the forecasts.

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

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

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;```

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

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.

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

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

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
}
```

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

kevgermany answered ·

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

Moving to modifications as it's Node Red.

1 comment

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

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

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