Hi all.
I thought I’d share my solution for intelligently charging my home battery that I’ve spent quite some hours on perfecting. I have it scheduled once every hour and when it runs it loops through the upcoming hours, checking the electricity price and simulating how the battery will charge/discharge. If the price is going up, it calculates how much the battery needs to be charged from grid now to minimize the risk needing to buy electricity later when it’s more expensive. It’s written in Rules DSL.
I doubt anyone will have any use of this straight off, but I hope the thoughts are useful to someone!
The things I have set up to make this work:
- Modbus communiation with my SolaX hybrid inverter. Modbus is the only way to control these inverters. I used this thread as a staring point. Also see here for SolaX remote control instructions. And I really don’t recommend trying to use Modbus TCP using SolaX wifi dongle, it’s crap. You need a Waveshare converter for the communcation.
- The excellent Energi Data Service Binding to get upcoming electricity prices. This one assumes that you live in either Denmark or in southern Sweden (SE3 or SE4)
- The equally excellent SolarForecast Binding to get a prognosis of solar energy production.
Actually the hardest part was trying to make a prognosis for power consumption. I have persisted data of consumption going years back, but I came to the conclusion that simpler is better, so now I just check consumption the same time yesterday and last week and use the highest of those, works fine this far.
The one thing I’m thinking about doing is completing the loop with the opposite, ie discharging the battery to grid if the price is due to go down, to not risk having to sell the energy for a lower price later. Haven’t really decided if it would be worth it though…
Complete rule follows:
val double batteryCapacity = 12.0; // kWh
val int minBatteryPercent = 20; // %
val int chargingUpperLimit = 9000; // Max speed for charging, W
val int chargingLowerlimit = 4000; // Min speed for charging, W
val edsActions = getActions("energidataservice", "energidataservice:service:635c20512c");
val sfActions = getActions("solarforecast","solarforecast:fs-plane:0e8335fb8a:c056ab1541");
val prices = edsActions.getPrices("SpotPrice");
val ZonedDateTime startTime = now.withMinute(0).withSecond(0).withNano(0);
val Number startPrice = prices.get(startTime.toInstant());
val Instant solarEnd = sfActions.getForecastEnd();
var int hour = 1;
var Boolean exitLoop = false;
val int currentPercent = (Solax_inverter_Battery_Level.state as Number).intValue(); // %
var double currentSOC = batteryCapacity * currentPercent/100; // The SOC at the simulated time, kWh
var double lowestSOC = currentSOC; // The lowest SOC point during the simulation, kWh
val double initialSOC = currentSOC; // The SOC at the start of the simulation (ie now), kWh
val double lowestAllowedSOC = batteryCapacity * minBatteryPercent/100; // kWh
//Loop one hour at the time, simulating what will happen in that hour
while(hour < 36 && !exitLoop) {
var Instant fromTime = startTime.plusHours(hour).toInstant();
var Instant toTime = startTime.plusHours(hour+1).toInstant();
var Number price = prices.get(fromTime);
if(price == null || price <= startPrice) {
logInfo("chargerule", "Cheaper or unknown price, ending loop!");
exitLoop = true;
} else if(toTime > solarEnd) {
logInfo("chargerule", "No more solar data, ending loop!");
exitLoop = true;
} else {
val float solarThisHour = (sfActions.getEnergy(fromTime, toTime) as Number).floatValue();
//Some guesswork here, guessing the consumption will not be lower than it was the same time yesterday
//or the same time one week ago.
val float consumptionLastWeek = (Elforbrukning_timme.persistedState(now.plusHours(hour).minusWeeks(1)).state as Number).floatValue();
val float consumptionYesterday = (Elforbrukning_timme.persistedState(now.plusHours(hour).minusDays(1)).state as Number).floatValue();
var float consumptionThisHour;
if (consumptionLastWeek > consumptionYesterday) {
consumptionThisHour = consumptionLastWeek;
} else {
consumptionThisHour = consumptionYesterday;
}
currentSOC += solarThisHour;
currentSOC -= consumptionThisHour;
if(currentSOC < lowestSOC) {
lowestSOC = currentSOC;
}
logInfo("chargerule", "Hour:" + hour + ", solar: " + solarThisHour + ", consumption: " + consumptionThisHour + ", SOC: " + currentSOC);
if(currentSOC < lowestAllowedSOC - batteryCapacity) {
logInfo("chargerule", "Max charge needed, ending loop!");
exitLoop = true;
} else if(currentSOC >= batteryCapacity) {
logInfo("chargerule", "Simulated battery full, ending loop!");
exitLoop = true;
}
hour++;
}
}
//The amount we need to charge in kWh
val double neededCharge = (lowestAllowedSOC - lowestSOC) + initialSOC;
//The amount we need to charge in percents
var int neededPercent = Math::round((neededCharge/batteryCapacity)*100).intValue();
if(neededPercent > 100) {
neededPercent = 100;
}
logInfo("chargerule", "Current battery level: " + currentPercent + "%, we need " + neededPercent + "%.");
//Check if we need to charge and also that the inverter is not alreay in a control mode
if(neededPercent > currentPercent && (Solax_Power_Control.state as Number) == 0) {
var int chargingPower = (batteryCapacity*1000 * (neededPercent-currentPercent)/100).intValue();
if(chargingPower > chargingUpperLimit) {
chargingPower = chargingUpperLimit;
}
if(chargingPower < chargingLowerlimit) {
chargingPower = chargingLowerlimit;
}
logInfo("chargerule", "Charging house battery up to " + neededPercent + "% with " + chargingPower + " watts.");
//Solax power control mode 3 which can charge up to a specific percentage
//is broken in my current inverter firmware version, using mode 4 instead
//Solax_Power_Control.sendCommand(3);
//Solax_SoC_target.sendCommand(neededPercent);
//Solax_Chargedischarge_power.sendCommand(chargingPower);
//Solax_remote_ctrl_timeout.sendCommand(60);
Solax_push_mode_power.sendCommand(-chargingPower);
Solax_Power_Control.sendCommand(4);
while((Solax_inverter_Battery_Level.state as Number) < neededPercent) {
Thread::sleep(30000);
}
Solax_Power_Control.sendCommand(0);
}