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

These are the two main objectives for me but the third one is to reduce the number of compressor starts to extend the compressor lifetime. My Nibe F1226 is not an inverter-pump that runs all the time but it’s a traditional on/off pump. For me this spot price optimization reduced the number of compressor starts by roughly 90% but not all of this can be attributed to the spot price optimization, a big factor is also that the water pump for domestic hot water is no longer running all the time as I mentioned earlier. But yeah, if we leave this frequency of compressor starts aside for a while, the main objectives to balance are the cost and comfort.

You’re making unsafe assumptions here, which is understandable because you live in a country which is way warmer than Finland. Here’s some context for you.

  • I live in Espoo, Finland, which is 800 km South from the polar circle
  • Our house is brand new, made of 375 mm stone which “buffers” heat very well. The walls are 375 mm thick and the U factor for insulation is 0,20 W/m2K.
  • We have triple glass windows
  • We have 200 m2 floor surface, 10cm thick, which means that there is 20 m3 of concrete that can buffer heat energy. There is 200 mm thick insulation between the concrete floor and the ground (250 mm thick insulation near the walls).
  • All in all, this house is probably better insulated than 95% of the houses in Central Europe.

Let’s look at some data so that we don’t need to rely on opinions as data does not lie.

On January 4th the heating stopped at 06:00 and the inside temperature peaked at 09:00 at 20.5 C. At 23:00 the temperature had decreased to 19.3 C. This decrease is noticeable, but we’re used to it and it doesn’t bother us too much

Our family is optimizing cost in favor of comfort so we can tolerate this 1.2 degree dip. But not all families want to take it to this extreme and has higher weight for comfort, which means that you can’t leave the house unheated from 06:00 until 23:00. Which means that you need to allow some amount of heating in between. Which means that you either do the scheduling manually, or you let the algorithm do it for you automatically. If you want to automate it, you need an algorithm that can select the least expensive hours before the absolute cheapest period starts at 23.00.

The slicing concept (selecting some hours also during the day time) is important in houses which are either not that well insulated OR which heats with some other means than heat pumps. One of the most common ways to heat houses in Finland is that you have electric heating where the heat cables are inside the concrete floor. This means that there is no water that would buffer the energy, but the concrete floor can “store” heat energy. But the buffering effect is less than with underfloor water heating.

Point taken. Still wondering though.
First, I’m amazed you see that much of a drop in a that well insulated house.
Yes data does not lie but it’s a one-day snapshot that may or may not be representative.
I wonder if it was a particularly cold day ? Did you vent more than other people or on other days ?
Is the measuring point representative for the house ? etc.
Don’t bother to answer, questions are rhetorical.

Second aspect, if I understood your setup right, you don’t have a heating buffer but only do ‘direct heating’. That buffer would normally be emptied mid-day to heat/keep the temp up without consuming electricity. It would get refilled during cheapest night times.
Note the heating consumer side has a big impact on this, too. If you have say radiators with smart thermostats you can automatically turn consumption down during the day when you’re off for work.
Night times are often cheap times, but as many people lower temp per default at night times, it’ll effectively result in no or just very little use of the cheap power, resulting in a lower temperature starting point for the day.

Third - move some levels up in abstraction - how representative is this type of pricing curve, i.e. how often will it apply that you have a Gauss distribution-like curve with a lengthy expensive middle period?
I’m actually becoming customer of a variable tariff provider starting March, 1st, and in preparation have been watching prices daily and my first impression is that cheapest hours are jumping around wildly and interval length varies.
Your selection of say 3 slices may be optimal for days like this, but what if that’s a unicorn (well, rare) ?
You’d be running well 300+ days of the year with a suboptimal choice.

No worries… The day is a good and representative example of a cold and windy day. I updated the screenshot of my previous comment because it had cropped so that the right Y-axis was not visible so the outdoor temperature scale was not properly visible.

Now you can see from the screenshot that the outdoor temperature was -5 and dropped to -15. However, it was also a windy day. When it’s both cold AND windy, the heat loss is faster. That’s why I have that third temperature line which represents the “wind chill compensated temperature”. It was colder than -10 from the beginning and went below -15 on that day.

The days are very different… The spot prices in Finland are heavily dependent on three factors in practice:

  • Demand: How cold is it i.e. how much of electricity is needed for heating.
  • Demand: Is it weekday or weekend. Industry will consume significantly more electricity during the weekdays compared to a weekend, which means that there are usually more cheap hours during weekends. But not always, because of the next point…
  • Supply: At the moment in Finland, the biggest variable (by far) in the supply side is the production capacity of wind power. This varies between 100 MW and nearly 5000 MW and the shape of this production curve is effectively a mirror of the price curve.

First of all, our house does not need heating at all for summer months so that cuts down half of that 300 days :slight_smile: But yes, your thinking-out-loud is valid. Our house (and our optimization objectives) need slicing only on the coldest days of the winter. I have that UI where I can easily adjust the number of slices to be something else than 1 which it usually is, see the screenshot in #165. Only on the most coldest days will I need to use slicing to force some heating during daytime.

Or do it with a rule based on e.g. weather forecast.

Mind you that that in turn is very specific to your setup.
Commonly a heat pump will be producing DHW, too. All year long.
In new well-insulated houses hot water can account for up to 50% year-avg of the electric energy that goes into your pump, and 100% in summer.
So 1 slice should really really be the default setting for everybody.

I currently don’t do it automatically based on the forecast, but it would be conceptually possible to do it like so. Maybe next winter :slight_smile:

Yes, of course. During the summer months I simply allow 2 cheap hours for domestic hot water heating and 22 hours of the day the compressor is resting.

Markus

Thanks, found the bug in the algorithm, I had a classic offset by one… I will publish an updated script this evening.

Has anyone yet been in contact with Entsoe about when the service is getting back to normal?
I changed the url mentioned above and now I see:

2023-02-17 14:20:19.821 [ERROR] [nhab.automation.script.ui.7847d62ba6] - entsoe.js: Exception parsing spot prices: Cannot read property "period.timeInterval" from undefined

Or is this a new normal and the entsoe.js should be changed somehow?

@antonmies see
https://m-transparency.entsoe.eu/news/widget

1 Like

@timo12357 the bug is in nibe.js

I’ve been refactoring the code and will publish new versions in the coming days but you can fix the bug by modifying your nibe.js directly. Remember to edit and save your rule after you have modified the nibe.js file, otherwise openHab will use the previous version of the file it has cached in its memory.

Change this line:

            priceSlices[i].splice(j, 1); // Removes price from the slice since it's used.

To this:

            priceSlices[i].splice(j, 0); // Removes price from the slice since it's used.

Cheers,
Markus

1 Like

It really seems like the update to the new endpoint and probably som backend upgrades isn’t goning as supposed. Sweden SE3 still doesn’t have prices for tomorrow, not much to do about that for the moment I suppose.

You can see the prices from Market data | Nord Pool
(Divide by 10 to convert from EUR / MWh to c / kWh and multiple by your value added tax).

Then write the spot prices to your database with the script snippet I shared yesterday.

Hopefully Entso-E can get their act together soon. This infrastructure deployment is quite TARFU…

Markus

I tried to run the script with

node scriptname.js

But that failed with

influx.js: Preparing to write 23 points to the database for spot_price
/etc/openhab/automation/js/node_modules/someroad/influx.js:178
    const http = Java.type("org.openhab.core.model.script.actions.HTTP");
                 ^

ReferenceError: Java is not defined
    at Object.writePoints (/etc/openhab/automation/js/node_modules/someroad/influx.js:178:18)
    at Object.<anonymous> (/etc/openhab/automation/js/node_modules/someroad/scriptname.js:28:8)
    at Module._compile (internal/modules/cjs/loader.js:999:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

1 Like

A kind feature request:

While you redo the code, if you have the time and inspiration, add a safety feature that copies the heat pump steering from the previous day to the next day if entso spot prices have not been obtained by 23:00.

Due to the update where dayahead prices might be delayed I’m on the hunt for a validation off dayaheadprices. In my EV chargers rules I modified the script so it defaults to 2 instead of 1 if it can’t settle any control value for tomorrow at 6am. I have a secondary data source beeing tibber that indeed just delivers current hour and maybe next hours price but that could be better than nothing. Or as Timo wanted if one validates dayahead prices and they’re not there then one could buil rules for using yesterday instead of today or whatever suites each and every one.

Right now I’m in the testing of a separate rule for just validating spotprices just to make sure that all automated dessicions is made when prices are known or if prices isn’t know at a specific time I the can control what’s supposed to happend.

Du too my lack of proffesional coding my example looks snippet below for the moment. With one thing needed to be fixed is to find a method of using now+1day as stop_tomorrow datetime. And I need to see if the statsu really becomes 1 or 0 when prices is updated.

This function is currently extracted from a bigger context and naming isn’t the best yet. In my opinion default values should be another than operational values, but that was easy to change. Maybe theres a better way to verify that now+1day’s prices exist or not exist.

dh = require('kolapuuntie/date-helper.js');
nibe = require('kolapuuntie/nibe.js');
influx = require('kolapuuntie/EV_influx.js');


evTarget=items.getItem("evTarget");
range = items.getItem("spotPriceRange");
start_current_hour = dh.getCurrentHour();
// If spotprice tomoorrow == ON använd tomorrow if of use midnight 
status = items.getItem("Spotprice_tomorrow");
start_item = items.getItem("Evconnected").rawState;
start = new Date(start_item);
stop_item = items.getItem("EvCarReady").rawState;
stop_tomorrow = new Date(stop_item);
n = 1
slices = 1;
min = 0;


points = nibe.determineHours(start, stop_tomorrow, n, slices, min);
influx.writePoints('carcharger_control_advanced', points);
console.log("EV control","EV Control finished");
console.log("EV control","antal timmar skrivna till influx: " +n);



control = influx.getCurrentControl('carcharger_control_advanced', stop_tomorrow);

if (control != 2) {
  status.sendCommand("ON");  
  console.log('Validate dayahead: Spotprice for tomorrow ok');
  range.sendCommand("Tomorrow");





  
}
else if (control == 2) {
  status.sendCommand("OFF");
  console.log('Validate dayahead: Spotprice for tomorrow not known');
  range.sendCommand("Today");
}
1 Like

@masipila, I have been following this discussion for some time because I will get Nibe S1255 heat pump next summer so most likely I will start using your code. I live also in Espoo.

Have you noticed that S1255 has a built-in feature called “Smart Price Adaption” which should do all this spot price optimization magic out of the box?

Yes, I know that S1255 has the spot price optimization but I don’t know yet how well it works. Our heat pump will be installed next summer.

A few days back I have started getting these errors in the log:

2023-02-19 08:17:59.068 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'fmi_forecast_temperature' in registry
2023-02-19 08:17:59.070 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'fmi_forecast_temperature' in registry
2023-02-19 08:17:59.073 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'fmi_forecast_temperature' in registry
2023-02-19 08:17:59.076 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'fmi_forecast_temperature' in registry
2023-02-19 08:17:59.078 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'fmi_forecast_temperature' in registry
2023-02-19 08:17:59.118 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'spot_price' in registry
2023-02-19 08:17:59.121 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'spot_price' in registry
2023-02-19 08:17:59.124 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'spot_price' in registry
2023-02-19 08:17:59.127 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'spot_price' in registry
2023-02-19 08:17:59.130 [INFO ] [b.internal.InfluxDBStateConvertUtils] - Could not find item 'spot_price' in registry

They do not go away even if I rerun the scripts. Pump control works fine, though.

@timo12357 this instruction that I gave you a couple of days ago is unfortunately not correct. I don’t normally need slicing but now that it’s freezing cold again I had to slice and spotted another bug.

This should now be providing correct results:

function determineHours(start, stop, num, slices, min) {
    console.log('control-point-optimizer-slicing.js: Searching for cheapest hours...');
    let selectedHours = [];

    // Read the spot prices from the database.
    const prices = influx.getPrices(start, stop);

    // Early exit if there if spot prices are missing.
    const duration = Math.abs(stop-start) / (60*60*1000);
    if (duration > prices.length) {
        console.error('control-point-optimizer-slicing.js: Not enough spot prices! Expected ' + duration + ' but found ' + prices.length);
        return selectedHours;
    }

    // Get the prices for each slice of the day.
    const priceSlices = slicePrices(prices, slices);

    // Pick minimum number of hours from each slice to the final array, but at least 1 per slice.
    let minPerSlice = Math.floor(num*min);
    if (minPerSlice == 0) {
        minPerSlice = 1;
    }

    console.debug('control-point-optimizer-slicing.js: Minimum number of hours for each slice: ' + minPerSlice + '. Picking these hours first...');
    for (let i = 0; i < priceSlices.length; i++) {
        console.debug("control-point-optimizer-slicing.js: Picking hours for slice " + i);
        for (let j = 0; j < minPerSlice; j++) {
            // Select the first (cheapest) hour from current slice and remove it from priceSlices since it's now been used.
            let selected = priceSlices[i].shift();
            selectedHours.push(selected);
            console.debug("control-point-optimizer-slicing.js: Picking " + JSON.stringify(selected));
            num--;
        }
    }

    // Select remaining hours purely by their price.
    const merged = mergeSlices(priceSlices);
    const rest = merged.slice(0, num);
    selectedHours = selectedHours.concat(rest);
    const unselectedHours = merged.slice(num);
    const points = preparePoints(selectedHours, unselectedHours);
    console.debug('control-point-optimizer-slicing.js: Control points:');
    console.debug(points);
    return points;
}

sorry for the hassle!

Nevermind! Thanks for constantly improving the software!