Rule to intelligently charge home battery

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);
}

4 Likes

It often helps to see the actual complete rule. That will show any triggers and conditions in addition to the actions as well as other stuff which might be relevant.

To show the full rule click on the code tab and paste the YAML you find there.

This looks a while lot like Rules DSL but given the popularity of rules options these days, it’s always appreciated when posters explicitly mention the language (note the language can be determined from the mine type in the YAML so if you post the YAML that’s good enough.

As a Rules DSL script I do see some things that are of concern.

  1. You should only use primitives in Rules DSL when it’s absolutely necessary, like in a call to a function that only takes a primitive. Leave numbers as Numbers the rest of the time. On an RPi, it could take minutes to parse and load this rule just because of all the primitives.
  2. Rules DSL tries (and fails sometimes) to be a typeless language. It really prefers to figure it the types at runtime instead of load time. Forcing the type of a variable when you define it will add even more time to load and parsing of the rule. And to make matters worse, it’s not guaranteed to remain that type anyway. If you do a calculation and save the result to a var float, that variable will no longer be a float, it’s going to become a BigDecimal regardless of how you declared it. By default Rules DSL will treat any number as a BigDecimal and it’s better to let it.
  3. An indeterminate sleep that can last for minutes is a really bad idea. If the rule triggers five times while this rule is during there sleeping for minutes, each trigger will queue up and be working off in sequence. You could end up with dozens of this time waiting to run effectively starving it out and making it unable to react to be triggers. And each trigger queued up is going to consume some resources. A timer or better choice of a rule trigger would be better.

Because of 1 and 2 I now recommend against new development of rules in Rules DSL. JS Scripting and jRuby are just as easy to code in now. Blockly is even easier. All three provide more complete access to all that OH offers and none of them have this half assed typing system.

2 Likes

Basically you’re absolutely right here, but here’s an improvement: don’t use yesterday’s value but use consumption of the same weekday, i.e. now.minusDays(7), to get a better consumption pattern match.

The road to a comprehensive energy management system is long, stony and full of pitfalls.
As you probably read in the Solax thread you linked, I’m developer of a commercial energy management system, also based on rules DSL.

A pro tip: it’s getting more and more complex and convoluted the more aspects of energy management you’re trying to make part of your calculations, like hours you heat or apply power limits in charging your EV. Particularly so when you move from a one-hour-at-a-time calculation basis to longer forecast/scheduling periods that include changing prices.
So having walked down that road in a long time, I’d strongly suggest not to dig into that rabbit hole. Trying to optimize that can quickly evolve into a full day job.
Better don’t. Unless you want to become a competitor, in that case you’re welcome to :rofl:

1 Like

I don’t really agree here. This may be valid for a simple rule which a newbie could just lift into their system and use straight off. This rule however, nobody could use out of the box without quite some editing and I’m sure anyone able to do that editing will also be able to set up the scheduling. Getting the complete YAML just seems like a whole lot of overhead stuff.

Not really sure what you mean by this comment… I mentioned in the post that it’s written in Rules DSL, should I have written this in some other way?

Yes, I’m still a bit confused about all the data types in Rules DSL. But what you’re saying is basically the best would be to stick to Number instead of int/float/double and also it’s better to declare variables without data type and let the rules engine itself decide the data type? And maybe drop some intValue and floatValue calls and let the variables be what they are?
I haven’t experienced this as a problem, the rule is really loading in microseconds, but of course it’s stupid spending CPU cycles on stuff that isn’t needed…

Yes, I know. This felt ugly no matter of language chosen. Actually this rule could be running for a full hour until the battery is charged… But in my defense it’s just a temporary workaround because of an inverter bug which I hope will be resolved by SolaX, normally I’d just call it with remote control mode 3 which simply tells it to charge to a specific percentage.

Yep, i heard you the last time :laughing: However most of this rule was already written when we discussed this. Next time I start a new rule from scratch I’ll look at some alternative. Maybe start looking at Blockly since I really don’t like neither JS nor Ruby…

I think you misread me or my code here, that’s kinda exactly what I’m doing. Except I check both yesterday AND one week ago and take the highest of both those…
Me and @rlkoshak actually had a discussion about this in some other thread, discussing strategies for getting the most accurate prediction based on outdoors temperature (which at least in Sweden is the most significant factor). I came to the conclusion though that it’s not really worth it digging through years of data, that this keep-it-stupid-simple approach is good enough.

I don’t really think this is different, ie it IS addictive. It’s just a choice of how deep in the rabbit hole it’s worth going. This is supposed to be my light weight EMS with just the things that I need. But who knows, maybe it will grow to be a full blown EMS and I’ll put you of out market :grin:

Yeah let the games begin.
Good luck! :wink:

PS I’m at roughly 4K lines of code now

I missed that. I guess I didn’t read closely enough.

I reread through the post is it was never clear to me until now that you need to use a cron trigger to drive the rule.

Yes. only use primitives where is required which is going to be stuff like calls to now.plusSeconds() and Math.round(). And I’m those cases you’ll use now.plusSeconds(myNumber.intValue) to get to the primitive.

This too. Just let Rules DSL figure out and maintain the type except in those cases where it needs help or gets it wrong.

There’s an old thread where we came up with a calculation which even on a powerful machine would take about ten seconds to load. On an RPi it took upwards of 15 minutes. Switching back to Number and load times returned to normal.

If it’s temporary that’s no problem. A looping timer could be a better implementation though. Instead of a while loop use a timer that reschedules itself.