Control a water heater and ground source heat pump based on cheap hours of spot priced electricity

Can you elaborate this use case a bit more? What exactly do you want to achieve? I understand that it’s related to charging the electric car, but what exactly is it that you want to optimize?

Hi

I beleive that I first of all need to make a new control unktion similar to the one for your waterheater but with a longer period of on. But thanks to your previois answers that will probably be within my knowledge now.

Im sorry for using wrong name for timezones, I schould have known better.

Thanks once again for all help.

No worries at all, the remark of UTC vs. GMT was just to prevent misunderstandings. Timezone handling is a pain in the rear, but it’s significantly less painful when the systems (including the code that we ourselves write) respect the ISO 8601 standard. :slight_smile:

I would really be interested how you are planning to optimize your car charging. We are still waiting our electric car, so I’m all ears. I can imagine that there are many different aspects to be taken into account in addition to just the spot prices, including the spot prices, how many hours you need for charging, when do you need the car to be charged, is the fuse a limiting factor i.e. are you able to charge with full power at the same time when other big loads are on and so on. So if you could elaborate on these real-world needs, I can then try to reflect what these existing scripts are already capable of…

Hello

I will try to elaborate my use case, here in Sweden we typically have a 6,20 or 25A main fuse per phase (3phases). From the first of september I will have a floating electrical price from previous where we had a fixed price 0,47SEK/kWh, within the upcoming 12 months we will also get a maxpower fee, if we could load or 25A fuses to just 20A the fee will become lower.

So in somewhat we will probably not want to max the fuses. My main concern upuntil recently hav been to protect mainfuse from overloading, but soon having floating electricalprice and this summer we also bought two bath tubs that also need electricity, and our EV car a Kia that chrages via Easee wallbox that ships with a simcard and wifi so one could connect internet in two way and they have a good api for integrating. I have one rule that handles charger when no car is connected and one rule that handles loadbalancing (this rules is pasted below).

I can’t figure out how to connect to Kia webapi rumors telling there’s an api, so it’s really hard to get som automatic reading of how many hours the car will need charging and my wife will not have to set the cars battery percent or similar somewhere it should just start and load typically during the late evenings or nights now it starts when powerconsumption in the house is setteled for a period of 30 minutes, the it increases as much the powerconsumption will allow I did set a spare buffer of 5A if I remember correctly.

Just to give you an idea of which rule I have I paste the loadbalancing rule below.

var lastIncTimer = now
var lastDecTimer = now
var loadInc = 6
var loadDec = 6
rule "EaseeBox Charging event based"

  when

  Time cron "45 */1 * * * ?"  or Item gridloadmax changed  //"0 */1 * * * ?"
  //Item gridload changed //powergridCurrentConsumptionL1 changed or Item powergridCurrentConsumptionL2 changed or Item powergridCurrentConsumptionL3 changed

then
logInfo("Easee", "eventbased rule started")
if (newDynamicCurrent.state==NULL){
    newDynamicCurrent.postUpdate(6)
}
var old_dynamic_current = easeeMaxCurrent
var loadDec = (old_dynamic_current.state as Number) -2
 //var loadDecx2 = (newDynamicCurrent.state) as Number -4
var loadInc = (old_dynamic_current.state as Number) +2
 
  if (availableCurrentCriticalTimestamp.state == NULL) {
  availableCurrentCriticalTimestamp.postUpdate(new DateTimeType())  
  }
  if(availableCurrentLowTimestamp.state == NULL){
    availableCurrentLowTimestamp.postUpdate(new DateTimeType())
  }
  if(availableCurrentMedTimestamp.state == NULL){
    availableCurrentMedTimestamp.postUpdate(new DateTimeType())
  }

  if(availableCurrentHighTimestamp.state == NULL){
    availableCurrentHighTimestamp.postUpdate(new DateTimeType())
  }
  if(currentOverloadTimestamp.state == NULL){
    currentOverloadTimestamp.postUpdate(new DateTimeType())
  }

 val timestampCritical = (availableCurrentCriticalTimestamp.state as DateTimeType).getZonedDateTime()
 val timestampLow = (availableCurrentLowTimestamp.state as DateTimeType).getZonedDateTime()
 val timestampMed = (availableCurrentMedTimestamp.state as DateTimeType).getZonedDateTime()
 val timestampHigh = (availableCurrentHighTimestamp.state as DateTimeType).getZonedDateTime()

  // increase dynamic charging power
  
  var stringLoadInc = (loadInc + ";" + loadInc + ";" + loadInc)
  var stringLoadDec = (loadDec + ";" + loadDec + ";" + loadDec)
  logInfo("Easee", "Easee loadInc 3 phase: " + stringLoadInc)
  logInfo("Easee", "Easee loadDec 3 phase: " + stringLoadDec )
  //Easee_DynamicCircuitCurrentsPhase123.sendCommand ("10;10;10")
  //Easee_DynamicCircuitCurrentsPhase123.sendCommand (loadInc + ";" + loadInc + ";" + loadInc)
            if (EaseeBox_chargerOpMode.state as Number == 3){
             logInfo("EV charger", "Loadbalancing started")
             logInfo("EV charger", "Increase step 2 Timestamp high: "+timestampHigh+" Timestamp med: "+timestampMed+ "Last inc timer: " +lastIncTimer)
            if (availableCurrentHighProxy.state ==ON && easeeMaxCurrent.state >= loadDec && now.minusMinutes(5).isAfter(lastIncTimer) || availableCurrentMedProxy.state ==ON && easeeMaxCurrent.state >= loadDec  && now.minusMinutes(5).isAfter(lastIncTimer)) { //(powergridCurrentConsumptionL1.state <=0 && powergridCurrentConsumptionL1.state >=-5 || powergridCurrentConsumptionL2.state <=0 && powergridCurrentConsumptionL2.state >=-5 || powergridCurrentConsumptionL3.state <=0 && powergridCurrentConsumptionL3.state >=-5){
             logInfo("EV charger", "Increase step 2 Timestamp high: "+timestampHigh+" Timestamp med: "+timestampMed+ "Last inc timer: " +lastIncTimer)
                 if (now.minusMinutes(30).isAfter(timestampHigh) || now.minusMinutes(30).isAfter(timestampMed)){
                //if (availableCurrent.state >=8.4 && easeeMaxCurrent.state >= loadDec && now.minusMinutes(5).isAfter(lastIncTimer) || availableCurrent.state >=8.4 && easeeMaxCurrent.state >= loadDec  && lastIncTimer == NULL) { //(powergridCurrentConsumptionL1.state <=0 && powergridCurrentConsumptionL1.state >=-5 || powergridCurrentConsumptionL2.state <=0 && powergridCurrentConsumptionL2.state >=-5 || powergridCurrentConsumptionL3.state <=0 && powergridCurrentConsumptionL3.state >=-5){
                logInfo("EV charger", "Increase step 3")
               if (newDynamicCurrent.state >=0 && newDynamicCurrent.state <=14){
                Easee_DynamicCircuitCurrentsPhase123.sendCommand (loadInc + ";" + loadInc + ";" + loadInc)
                //EaseeBox_dynamicChargingCurrentL1.sendCommand (loadInc)
                //EaseeBox_dynamicChargingCurrentL2.sendCommand (loadInc)
                //EaseeBox_dynamicChargingCurrentL3.sendCommand (loadInc)             
                logInfo("Easee", "Tillgänglig ström ökad till: "+loadInc+" Amp")
                postUpdate(newDynamicCurrent,loadInc +2)
                lastIncTimer = now
     }
    }
  }          
}





if (availableCurrentLowProxy.state ==ON && now.minusSeconds(90).isAfter(lastDecTimer) || availableCurrentCriticalProxy.state ==ON && now.minusSeconds(90).isAfter(lastDecTimer)){  // if (powergridCurrentConsumptionL1.state <=-6 || powergridCurrentConsumptionL2.state <=-6 || powergridCurrentConsumptionL3.state <=-6){
  if (now.minusSeconds(60).isAfter(timestampCritical) || now.minusMinutes(10).isAfter(timestampLow)){
      logInfo ("EV charger", "decend step 2")
       //if (availableCurrent.state <3.5 && now.minusSeconds(90).isAfter(lastDecTimer) || availableCurrent.state <3.5 && lastDecTimer == NULL){  // if (powergridCurrentConsumptionL1.state <=-6 || powergridCurrentConsumptionL2.state <=-6 || powergridCurrentConsumptionL3.state <=-6){
        logInfo ("EV charger", "decend step 3")
        if (newDynamicCurrent.state <=18 && newDynamicCurrent.state >=2){
              //easeeRequestBody = '{"dynamicCircuitCurrentP1":'+loadDec+',"dynamicCircuitCurrentP2":'+loadDec+',"dynamicCircuitCurrentP3":'+loadDec+'}'
            logInfo("Easee", "Tillgänglig ström sänkt till: "+loadDec+" Amp")
           Easee_DynamicCircuitCurrentsPhase123.sendCommand (loadDec + ";" + loadDec + ";" + loadDec)
            //EaseeBox_dynamicChargingCurrentL1.sendCommand (loadDec)
            //EaseeBox_dynamicChargingCurrentL2.sendCommand (loadDec)
            //EaseeBox_dynamicChargingCurrentL3.sendCommand (loadDec)
              postUpdate(newDynamicCurrent,loadDec -2)
              lastDecTimer = now
        }
    }
    else
              logInfo("Easee", "Loadbalancing performed but not charging")
    
    }        
end

Thanks for the additional info! So if I try to summarize with my own words:

Your loads are:

  • Charging the car
  • Heating your house with a ground source heat pump
  • Heating the water in your boiler / waterheater
  • Heating the bath tubs
  • All other loads such as dish washers, washing machine, everything else

Your objectives are:

  • You must avoid overloading the fuse above 25A
  • You prefer to keep the fuse a bit below 20A
  • You want to optimize the power consumption of all loads to the cheapest hours

To plan this, you need to first plan which loads can be “on” at the same time.

  • Does your power company have a web portal or something like that where you could see what kind of power consumption you have on different hours?
  • If I were in your shoes, I would first see how much power your house consumes when all the big loads are off. As an example in our house, the air conditioning, fridge, freezer etc. consume about 0.4 kW during the summer. Note that the AC machine uses more power during the winter so that it does not freeze.

After you have seen the baseline, you can calculate the current I = P/U for different load combinations, assuming that your car charger, heat pump, water heater and bath tub heater all use all 3 phases equally.

  • When you know what loads can run at the same time, you can plan a strategy in which order you want to schedule the big loads.
  • Once you have a strategy for the schedules, then you can think of how to implement that strategy in openHab and Easee

This sort of control needs quite some planning and it’s even harder to implement it in a generic fashion.
Some big loads you can shut off (like a SG ready charge signal, handing local control to the heat pump), some you cannot (say the oven), but for most of them, it depends on the situation, like an EV. The EMS usually will not know if it can interrupt charging (like today you REALLY need the EV to charge because you need it for a tour while normally you’d just want it to be charged to some 80% throughout the night).
A washing machine you may interrupt depending on time that has passed since start and on personal need when you need it to finish.
Loads are also dynamic in nature: a washing machine or dish washer for example takes ~2kW for some minutes only. Plus all the loads you don’t know and cannot control, like the kitchen devices. That’s really difficult to forecast/schedule.

Also keep in mind it is very challenging to determine a ‘scheduling algorithm’ that also meets people’s life needs. What if your load is ‘optimized’ to 19A and you need to do the cooking ?
If you feel cold or hot or the EV isn’t charged or the washing ain’t done when you expected it to, users (in particular other persons on your household) will be annoyed quickly and start demanding changes from you or manually override things, thwarting your nice optimizations.

Ultimately, the best advice is to really not overengineer things.
(I know since I’m providing a commercial energy management system based on openHAB, see e.g. Looking for energy management system testers and have experimented with that a lot).

Hi

Yes your quite right just that I wille leave smaller loads up to enduser to determine buy their opinion (not much can beat the human brain)

I will only take automatic control over big consumer such as

  • EV charger (easee wallbox via yet unoficcial binding)

  • Heatpump 3phase boiler I will not laborate with the compressor unit due to excessive shutdown and start may shorten compressor liftetime) This is controlled via a Fibaro Z wave smart implant just closing a circuit in the heatpump aux panel.

  • Bathtubs one swimspa unit with two balboa controllers and 1phase heater element each integreated via balboa wifi module and python scripts, where I can set temprange to low and make sure tha temperauture can drop safley even in winter time.

Objectives will be
A variable for allowed peakpower from my mainfuses wich I have in a powergridload that based on Aeontec zwave energymeter 3phases. That one meassuers all consumers in our house.

Extra control items for cheapest daytime eg 06:00-17:59 and one for cheapeast night time 18:00-05:59 And I assume car charging will mostly take place in late evenings and nights, and hot tub may be heated during day hours.

But as @mstormi points out generall solution will fail and to specific solutions will not be robust because life happends, so in shortly I rather pay for electricity than having a debate of wy can’t we do any laundry now one might end up doing all the stuff in the household for savings.

I’ve had this working with charger and bat tubs now uses @masipila waterheater script so my tubs just runs a couple of hours aday during this warm period, in the winter times there will be different times leading to, probably manual heat tubs a little more when price are really low and not heat everyday, if no bathing is to be done winter bathing require som planning of other stuff. Or maybe just heat bathtubs if electrical price is underX sek kWh but only if temperature is safe above freezingpoint.

@mstormi

I would just say that I totally agree with you thoughts about general solutions and even more the to specific solution. All this stuff will probably be hard to commercialize without having to make alot of adjustment. One need to find that familys do’s and don’ts so life can keepon without to much interruption. I really would have preferd to have type 4 charging so I could read battery status of the car via charger, but I don’t think I can and Kia doesn’t provide an open API but there is reverse engineering beeing done.

In my oppinion really fun that we are 3x Marcus in this thread now.

@mstormi I second everything you wrote.

For the sake of clarification:

  • I was not suggesting that the dish washer or washing machine would be controlled by openHab.
  • I mentioned them because they are significant in terms of spot priced electricity contracts and they create consumption peaks which is good to take into account if the fuse soft 20A and hard 25 limit are of concern.
  • Most dish washers have a timer so that you can make it start say at 02:00 when the electricity is the cheapest on that day.
  • Our washing machine also has a timer which can be used to tell the washing machine when you want the program to be ready. We use this regularly to schedule the laundry so that the program finishes at say 06:30 when we wake up.

My experiences so far is that the openHab-control of the compressor is significantly reducing the number of compressor starts. I have two AUX inputs in the Nibe F1226 heat pump that I can configure in the Nibe menus. I have configured them as follows:

  • Aux 1: Prevent compressor start
  • Aux 2: Do not heat tap water

With the combination of these, I can allow the compressor to start for house heating purposes but not for hot tap water purpose. Or deny both.

Here’s the strategy for my heat pump & tap water heating:

  • Our total hot tap water capacity is 180 + 300 liters. This capacity is sufficient for 2 to 3 days, so I can easily heat the hot water on the most cheapest hours. This applies to both Nibe integrated 180 l tank (Aux 2) and the external 300 l water heater. I use the water heater script for both of these.
  • This alone reduces the number of compressor starts significantly because the tap water will be heated at one go, once per night. If I take a shower in the morning, the compressor won’t start to heat hot tap water until next night.

Aux 1 for house heating
This will optimize the hours when the house will be heated. Also here my assumption is (to be proven by data when the heating season starts), that the compressor run-times will be longer but there will be less starts.

Yes they are good candidates to postpone for optimization.
But devil’s in the details. Did you know that a common laundry run consumption is like
20W for 15 mins - 2000W for 20 mins - 50W for some 2 hours ?
The 20 mins vary based on laundry temperature (how long it takes to heat the water).

Chances are high you don’t hit the sweet spot.
Long story short, my opinion is it isn’t worth trying (unless you are VERY attentively watching your consumption and are willing to check and adapt over and over).

Unless you have a battery. That’s a gamechanger particularly w.r.t. maximum amperage but it’s
also easing scheduling a lot and just like with a hybrid car (which has a very small battery only) you can substantial savings just because your scheduling doesn’t have to exactly match your consumption (which is a predicted one hence subject to forecast failures anyway).
You don’t need a largish battery.

Thing with these is, you have to manually rework all of your scheduling when spot prices change, what if you don’t notice in time ?
You have to be “always on” for changes. For a consumer product, that would be inacceptable, and even for an enthusiast with his very own installation, having to keep up is annoying.
Ans what about living need dynamics, like if 02:00 isn’t acceptable because you urgently need some specific clothing. And what if your laundry is done at 6:30 but you forget about it when it’s done?
As I said, devil’s in the details.

Funny enough, I am nonetheless offering this with my EMS. I provide EMS-side presettable “must be finished by XXX”. You load and start the machine and EMS auto-shuts it off when there’s a better time. But you really need the ability to override start at any time. I am providing an Alexa-linked item for that. And I am sending a ‘laundry is done’ notification to the user’s phone, even with reminders when they do not confirm it (ever forgot your still wet laundry for a whole day ? Ugh …).
Am I contradicting myself (overengineering) ?
Let me know what you think about ‘applicability’ of that.

P.S.: don’t waste your money on smart home controllability of your white goods if they charge extra for it. Device-driven optimization is inferior to EMS (household wide) driven and will even interfere with that.
(and P.P.S.: did you know how many watts white goods are drawing in timer mode ? It’s more efficient to shut it off. Plus that works with any white good.)

I’m actively watching the spot prices on daily basis, because my electricity price depends on them, see comment #27.

On this week, prices have been jumping like crazy. On Wed and Thu nights this week there were several hours when the spot prices were practically free (cheapest hours were 0.006 c/kWh) but on Monday, the spot price was peaking at 107 c/kWh. That’s a 17833 x price difference.

If my weighted average (consumption on a given hour vs. spot price of that hour) is below the month’s average spot price, I get the delta as a discount, see details from the same comment #27. At the moment, I’m 6.7 c/kWh below the average in the month of August, which means that I’m paying 14.8 - 6.7 c/kWh if I can maintain the -6.7 delta until the end of the month.

If you look at the graph below, you can see that there are typically 1-5 very cheap hours every night. Green line = spot prices, scale on right axis. Blue bars are our consumption, left axis.

I can easily hit the dish washer’s water heating to a cheap hour using the “delayed start” feature of the dishwasher. Even if the eco-program is 3,5 hours, the water heating happens during the first hour. This has nothing to do with openHab, I simply use the control panel of the dish washer and program it to start at the desired time.

What comes to the laundry machine, we usually use it when the electricity is cheap between 04-07 so that the machine finishes the program around the same time (or a bit before) when wake up so that we can hang the clothes immediately. Again, this has nothing to do with openHab, our washing machine also has a “delayed start” functionality in the control panel. This isn’t exact science either, but if there are 4 cheap hours I can easily hit one of them.

As mentioned, these have nothing to do with openHab, we simply use the “delayed start” functionality of the dish washer / waching machine. We always check the next days spot prices when starting them, it has become a habit. And of course we wash the dishes / clothes during the day if we need to.

And no, I haven’t checked how much the machines consume on the couple of hours in the evening (after I go to bed and between the time they start with the “delayed start”), but that’s insignificant because the benefits outweight the small consumption for sure.

Cheers,
Markus

Sure thing.
What I wanted to promote actually IS to have openhab control your machines.
Get a 25€ Shelly plug. Advantages are reliability and, worth even more, peace of mind:
you don’t have to watch the spot prices. OH always knows when spot prices changes. It does not forget to delay nor err in point of time for an optimum start. Plus it reminds you to remove the laundry. For me that’s well worth some 50€ all by itself.

Sorry I was somehow (wrongly) assuming everybody has a PV system (in Germany most do but in turn few have variable tariffs) but now realize this is not a prerequisite for this thread.
Then again if you have PV, that in turn is another dimension of optimization and control complexity because while weather/yield can be forecasted, sub-minute excess power isn’t predictable.
You really benefit from a battery a lot in that scenario in terms of ‘smoothening’ consumption.

Sounds interesting… Do you want to elaborate what PV stands for so that the community can understand the possible future readers / commentators point of view?

Cheers,
Markus

Photovoltaic, producing your own power that is. When you feed the grid you get a lot less money than you save when you run your devices with that power you generated.
So the problem class is quite similar to that of this thread and so are the possible solutions (increase self consumption by moving loads into times of excess solar power).
The challenge with PV power however is that you never know upfront when there will be excess and how much.

Interesting differences between the countries… My neighbor has solar panels and they have been optimizing their home pretty much to the max already for years.

In Finland, it used to be so that it was significantly better to consume the power generated by your solar panels yourself. Our neighbors were optimizing for this in the past.

Now that the spot prices are sky high during daytime and very low during the nights, it’s MUCH more profitable for them to sell as much of the solar power they can (minimize their own consumption during daytime) and then purchase cheap electricity during the nights to heat their own water, for dish washer and laundry.

In Finland you can sell your solar power back to the grid and the price you get follows the Nordpool spot prices.

Cheers,
Markus

Indeed. Germany suffers a lot from regulatory lobbyism. There’s very few providers of variable tariffs, some do not even accept new customers at the moment, and you have to pay a high base fee per kWh for transmission so savings are way lower than they could be.
There’s a static feed-in limit and you need permission from your transmission provider to feed the grid from battery.
Things started to move but adoption is slow and changes only ever apply to new installations so not the vast majority of people.

HI

I will just confirm into big differences even if we live quite close, In sweden the government hands out extra 0,6 or 0,7 SEK per kWh, however it’s a big investment about 30-50’ EUR and that’s without battery, government has a handout for up to 50% if your lucky and they still have funding. I played with the idea of getting a battery and charge that when power is cheap but I don’t know if thats a good investment.

One more post before other activities I made a rule script that actually a copy of the waterheater script but I wan’t 12 hours on time and a control item that’s called carcharger_control, I just changed item name to write to influx and that seems to work Carcharger_control is the blu line and spot price in SEK or atleast 1/10 of EUR /kWh (static exchangerate makes it easy to comp) But I’m not sure if the script really does what I wanted it do do.

Script that executes every hour

   dh = require('kolapuuntie/date-helper.js');
        wh = require('kolapuuntie/carcharger.js');

        start = dh.getMidnight('start');
        stop = dh.getMidnight('stop');

        wh.determineHours(start, stop, 12);

The carcharger.js script that rule script exec, sorry for only changing item to write to influx and not all log lines and stuff.

/**
 * Javascript module for waterheater helper methods.
 */

/**
 * Exports.
 */
module.exports = {
    determineHours: determineHours
};

/**
 * Calculates the on/off hours for the waterheater and writes them to the database.
 *
 * @param Date start
 *   Start of the time range where to find the cheap hours.
 * @param Date stop
 *   Stop of the time range where to find the cheap hours.
 * @param int num
 *   Number of hours the waterheater must be on.
 */
function determineHours(start, stop, num) {
    console.log('waterheater.js: Starting to calculate on/off hours for the waterheater...');
    influx = require('kolapuuntie/influx.js');
    const prices = influx.getPrices(start, stop);
    const startIndex = findStartIndex(prices, num);
    const points = preparePoints(prices, startIndex, num);
    influx.writePoints('carcharger_control', points);
}

/**
 * Finds the cheapester hours for the waterheater.
 *
 * The power must be on for 'num' hours in a row so that the waterheater
 * can reach it's max temperature. Reaching the max tempereature is important
 * to avoid legionella bacteria. 
 *
 * @param array prices
 *   Array of point objects containing prices for each hour.
 * @param int num
 *   Number of hours the waterheater must be on. 
 *
 * @return int
 *   Index of 'prices' array when the heating shoud start.
 */
function findStartIndex(prices, num) {
    let cheapestSum = 0;
    let cheapestIndex = 0;

    for (let i = 0; i <= prices.length - num; i++) {
	let sum = 0;
	// Calculate the sum of the hourly prices for the current 'num' hours.
	for (let j = i; j < i + num; j++) {
	    let point = prices[j];
	    sum += point.value;
	}
	// Initial value for the cheapestSum.
	if (i == 0) {
	    cheapestSum = sum;
	}
	if (sum < cheapestSum) {
	    cheapestSum = sum;
	    cheapestIndex = i;
	}
    }
    console.log('waterheater.js: Cheapest index: ' + cheapestIndex + ', sum: ' + cheapestSum);
    return cheapestIndex;
}

/**
 * Prepares the control points to be written to the database.
 *
 * @param array prices
 *   Array of point objects.
 * @param int startIndex
 *   Index of the 'prices' array when heating shoud start.
 * @param int num
 *   Number of hours the waterheater must be on.
 *
 * @return array
 *   Array of point objects.
 */
function preparePoints(prices, startIndex, num) {
    let points = [];
    for (let i = 0; i < prices.length; i++) {
	let value = 0;
	if ((i >= startIndex) && (i < startIndex + num)) {
	    value = 1;
	}
	let point = {
	    datetime: prices[i].datetime,
	    value: value
	}
	points.push(point);
    }
    return points;
}

Hard to tell even but you can do the math and have a big excel sheet to take everything into account (battery cost, degradation, government handout, etc.).
Most of the time for variable tariffing, the answer is ‘no’ because you have to pay for transmission twice.
Remember there’s more business models a battery enables you for: you can run on free/very cheap self generated power even in times without pv production (night, winter).
You will save all the transmission cost (in and out) for any self-generated-self-consumed kWh.
If unsure or a tight decision, you might want to wait for mobile batteries a.k.a. EVs with bidirectional charging (where the ‘battery’ has more benefits than just to help with ‘hedging’).
A battery can also protect you from grid outages (for my wife that was the reason to go for it).
I’m getting off topic now.