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

Ta-daa, I now had the time to publish the new version as a npm package. Fully updated documentation and usage instructions are documented at:

Some notes for the users of the earlier solution:

  • The basic concept is still the same i.e. there are control point Item for each device and then a separate Item which actually toggles the device ON/OFF (or to any other state)
  • The documentation instructions of the new version harmonize the naming conventions for these Items. If (most probably when) you want to keep using your existing data, pay attention to the Item names. For example, the instructions in the wiki use the name SpotPrice whereas it was spot_price in the earlier versions of the instructions. You can use the new solution, just replace SpotPrice with spot_price (or whatever is your existing item name.
  • The new solution is completely re-written to use js-joda date and time handling, there is 0 places left that use the shitty vanilla javascript date and time functions. If you want to tweak the scripts to work for your use cases, here are two excellent resources: JavaScript Scripting - Automation | openHAB and Working with Date Times in JS Scripting (ECMAScript 11)
  • The earlier “heating slice” concept is no longer available, I replaced it with PeakPeriodOptimizer which is simpler and is working much better at least in our house.

I will update the comment #13 (marked as solution) so that I’ll remove the files and rule snippets and link to the wiki documentation instead.

Happy optimizations to everybody and many thanks for the cheers so far!


I handle it so that even though the solution uses 15 minute resolutions, I still find cheapest periods (or block the most expensive periods) of desired lengths.

For example for heating the domestic hot water, I have an Item which I can use to define how many hours will be needed (it’s usually 2, I change it to a higher value if there is a lot of bathing or sauna party or something like that). Then I use optimizer.allowPeriod(N) where N is the number of the Item and block the rest. This will find the cheapest consecutive 2 hour period if the Item is set to 2.

There is also an algorithm optimizer.allowIndividualHours(N) which can be used, it will find N individual 1 hour periods which do not have to be consecutive.

For heating the house, I use the PeakPeriodOptimizer. This concept is fully documented here:



Great work, as usual, @masipila. As I have a well working system based upon your previous code I am very hesitant to migrate to the new system. What drawbacks and benefits are associated with migrating?

@timo12357 : Nothing of course forces you to change a working setup that you have.

There are a couple of benefits in the new version. The approach is now much more generic than in the first evolution. In the first version, the approach was quite tightly coupled to the water boiler and the ground source heat pump, starting from the name of the javascript library files. Now the approach is much more generic.

The second thing is that the 15 min resolution means that for example the heating optimization does not have to be full hours. For example for today, my solution is heating the house for 8,5 hours, not 8 or 9.

The third thing is that now the concepts are implemented using Duration objects. If the 15 min resolution kicks in next year in the day ahead market, then you really need to have this, at least I don’t want to toggle the heat pump on and off every 15 minutes.

If you want to try this, you have two main approaches: You can either set up a test environment or you can build the new Rules in parallel to your existing Rules.

For the test environment, I use Docker.

Here’s my Dockerfile to build the ohab image which is based on the official openhab:4.1.1 image so that it includes nodejs and npm

FROM openhab/openhab:4.1.1

# install node.js and npm
RUN set -eux; \
  apt-get update; \
  apt-get install -y --no-install-recommends nodejs; \
  apt-get install -y --no-install-recommends npm;

Here’s my docker-compose.yml:

version: '2.2'

    image: "ohab:latest"
    restart: always
      - "8080:8080"
      - "8443:8443"
      - "./openhab_addons:/openhab/addons"
      - "./openhab_conf:/openhab/conf"
      - "./openhab_userdata:/openhab/userdata"
      CRYPTO_POLICY: "unlimited"
      EXTRA_JAVA_OPTS: "-Duser.timezone=Europe/Helsinki"
      OPENHAB_HTTP_PORT: "8080"
      OPENHAB_HTTPS_PORT: "8443"
    image: influxdb:latest
    container_name: influxdb
      - 8086:8086
      - ./influxdb:/var/lib/influxdb
    restart: always

For the parallel implementation in your PROD environment:

  • Make sure that you make a backup of your node_modules directory, which contains your current scripts
  • Then install the new openhab-spot-price-optimizer. Restore your old scripts so that your current scripts continue to work.
  • You can then copy-paste a new Script which uses the new openhab-spot-price-optimizer scripts to test how it works and then fine-tune that as much as you want.
1 Like

Just found this super cool service that has a machine learning model to predict Finnish spot prices with an incredible accuracy…

I was thinking to experiment with this so that we could bring these forecasts to the same influxdb and use these predictions in our optimizations. Let’s see if I find time for this and if I can figure out any sensible use for it…


I’m really interested in this project! I’m also (eventually) going to have a heat pump, and would like to do some clever controlling.

However, when reading up online, most people advice against disabling the heat pump, claiming it will use a lot more energy for the same heat. Presumably because the pump will use direct heating when turned on again, to reach the desired level fast.

Do you have any thoughts on that? Are some heat pumps prepared for this, and others not?

My experiences do not support such claims. Having said this, our house is new, very well insulated and we have 200 m2 of 10cm thick concrete floor which reserves the heat extremely well. The walls are made of 37.5 cm thick siporex which also has great capacity to reserve heat.

I have disabled the direct heating in Nibe menus. It only kicks in in the ultimate fallback scenario where the outgoing water temperature is under 17 degrees. This is the last line of a failsafe defence which prevents the house from freezing if everything else has failed.

Too much cooling needs to be prevented though, because it takes time for a 200 m2 house to heat. That’s why I developer the PeakPeriodOptimizer algorithm which guarantees configurable amount of heating period between the price peaks. Also the number of peaks to block is configurable so it should be fit for purpose also for houses which does not reserve as much heat as our house.

There are two kinds of ground source heat pumps. Traditional on/off pumps where the compressor is always on or off. And inverter pumps, where the pump is always on but it adjusts the speed. I only have experience with on/off pumps, but the same optimization concept can be applied with a modern inverter pump. You could configure it for example so that when the prices are high, you adjust the target temperature down, and when the prices are low, you adjust the target temperature up.

I would assume that all modern ground source heat pumps are also SG Ready. SG is a vendor independent industry standard which has 4 modes. How the pump behaves in each of these 4 modes are usually configurable, for example so that when you set the pump for example to “now it’s cheap”, the pump will adjust the target temperature higher than usual.


Oh, and forgot to mention one more thing. Our heat pump continues to circulate the water in the underfloor pipes even when the compressor is blocked. So it will continue to heat the house for quite some time after openHab blocks the compressor.

Do I interpret this correctly that you are building a new house? If we would be building a house now, the only thing I would do differently is to have same thousands of liters water buffer where the heat pump would store energy and the underfloor heating would then consume the heat from this buffer. That way you could really heat this buffer on the absolute cheapest times of the day. These kind of buffers are quite common here in Finland, it’s called “puskurivaraaja” in Finnish if you want to Google it.

If my memory serves me well @ruxu or @timo12357 has this kind of thing?


Yes I do. Our heatpump is a Nibe 1245 R8 ground source heat pump that has only on and off switching for the compressor. Works great, and never heard of it using the direct electricity for heating. The pump power is so great that it is a lot faster than direct electricity.

One recommendation if you are building a new house with underfloor heating using circulating water. Invest in a thicker floor, about 20 cm. This will give you a thermal storage that will last for roughly 24 hours. Typically, when desigining water based under floor heating, construction engineers in Finland design about 8 cm floor depth. This is not an optimal thermal storage for a system such as the one @masipila has designed as it expects heating every few hours.

Thanks for all the info! I will definitely look out for “SG ready” then. If even the standard supports blocking the power, there cannot be any principal issue with it.

Probably the warnings refer to turning the power of the entire heat pump off.

It’s not a new house, but it currently has direct electricity heating. Unfortunately there are regular radiators rather than floor heating, so the heat storage mass is not comparable to yours. I have thought about a large water accumulation tank, and from your diskussion it sounds that would be the way to go.

Yeah, you kinda need something that can reserve the thermal energy if you are planning to skip the heating for based on the spot prices. If you have just radiators heating the air, it will cool down quite quickly if you don’t have a tank that can preserve the energy.

If you’re about to have a ground source heat pump, an additional hint. After the depth has been calculated be an engineer, make it a bit deeper than needed. If you ever need to change the pump and the new pump is more powerful than the original, the dwell can freeze if it’s not deep enough for the new pump. A colleague of mine is suffering from this and as a result, the new pump needs to run in shorter intervals than would be ideal.

As an example, our dwell was calculated to 150 meters but we paid a bit extra to make it 170m. According to the logs, it has never gone to minus degrees even though it has sometimes been running constantly for a 24 hour period when it was freezing cold…


Hi @masipila ,

i started using your script this weekend. Thank you very much for your efforts!
Unfortunately i get an error that i can’t get fixed.
I’m not using your influxdb implementation. I fetch the future data using the OH REST API. I will post my rulescript as soon as everything works.

Here is the log:

As you can see the window is from 00:00 2024-03-05T00:00Z until 2024-03-05T23:00Z
The script tries to set a controlpoint at 2024-03-05 22:00. Of course there is no point available.
Any idea how to get this fixed? Or is this more of an warning thatn an error?

Here is my points datafield:



can you elaborate a little bit more on your context.

  • What are you trying to achieve?
  • What parts of the setup you have been able to complete? (e.g. fetching of the spot prices is confirmed to be working)
  • What is the part that you are stuck with?

The second screenshot looks very weird as there is a mixture of thigs there.

  • epoch timestamps (time) do not belong here, where did they come from?
  • states do not belong here, where did they come from?

Dear Markus,

first of all: I want to control my heatpumpt with your script. The script is running for a few days now and it looks quite good at all.

As i told before I’m fetching the future prices using OH REST API. I’m NOT using your implementation for influxDB. The OH REST API returns an array with value and time in epoch timestamp. I’m than conferting this Epoch timestamp to an timedate zoned format like you are using in your scrips.

The array i postet is what i get back from the peak-period-optimizer using optimizer.getControlPoints().
This looks like it returns the same array you send to the script using optimizer.setPrices(x) but adding the control values.

Here is the idea of my script. I’m leaving some code blank where the REST API stuff is done as i need to clean this up.

PeakPeriodOptimizer = require('openhab-spot-price-optimizer/peak-period-optimizer.js');
optimizer = new PeakPeriodOptimizer.PeakPeriodOptimizer('PT1H');
start = time.toZDT('00:00');
start = start.minusHours(1); // I added this caus my future-time is only set until 23:00 
if (time.toZDT().isBetweenTimes('14:00', '23:59')) {
  start = start.plusDays(1);  
stop = start.plusDays(1);

previousDayStart = start.minusDays(1);
previousDayStop = start;


var heatingHours = Math.round(items.WP_HEATING_NEEDED.numericState);
var midHours = Math.round(items.WP_MIN_TIME_BETWEEN_HEATING.numericState);
var peaks = Math.round(items.WP_PeaksTOBlock.numericState);

optimizer.setOptimizationParameters(heatingHours, midHours, peaks);
points = optimizer.getControlPoints(); //<- This throws generic-optimizer.js error that it can't set controlpoint at time x (eg. 2024-03-04T22:00:00Z)

//console.error(JSON.stringify(points)); //<- This is what you have seen in my second screenshot before

Even if it throws the error it looks like the values get normally set:

Blue is Energyprice
Green is when Heatpump is allowed
Red is the frequecy output for the heatpump
the other values are only some feedbacks for me to see how good everything works.

One more information: I’m using a 1000 liter water stoarage to store the energy between price-peaks.

As you can see it looks like everything works great. The only weired thing is, that the generic-optimizer.js tries to set a control-value outside of the selected time-range what throws the error in Screenshot 1 on my post before.


Okay, now I think I got it.

The fact that the one entry is logged as an ERROR rather than WARNING is hugely misleading and it’s normal under some circumstances.


  • Let’s say that the most expensive peak is at the very end of the day so that the day ends in a block
  • The PeakPeriodOptimizer.blockPeaks blocks this period
  • And then it attempts to allow the surrounding periods just before and after the block
  • But since the peak was at the very end of the day, there is no prices available for the period just after the block. In this case the script logs an entry (misleadingly as an ERROR where it should be just INFO)

The same example applies when the peak that is blocked is at the very beginning of the day, the script tries to allow the surrounding periods just before and after the block, but since the block was at the beginning of the price window, the dataset does not contain the hour that would be allowed. I believe this is what you were facing because the first record in your dataset was 2024-03-05T00:00:00Z and the error was for 2024-03-04T22:00:00Z. Right?


Yes you got it.
I only wanted to get shure that this is an WARNING only.


1 Like

I would like to send a telegram message if the cheapest period price falls below a set threshold. I know how to send a telegram message, but is there already somewhere in the scripts a hook I could use to trigger the message?

You can achieve it like this:

Create a new instance of the Influx and GenericOptmizer classes like in the usage examples.

Read the spot prices for the period of interest, for example tomorrow midnight to midnight like in the usage examples.

Pass these prices to the GenericOptimizer using the setPrices method like in the usage examples.

Find the cheapest period with a duration of your choice with calculatePeriodPrices method. The first element of the returned list is the cheapest period. If the price of that element is cheaper than your threshold, send the Telegram message.

1 Like

Version 2.0.7 released with 1 minor improvement and 2 bugfixes.


  1. Change FMI API from Harmonie model to Edited model

FMI now has an API for a weather forecast which has been produced by a meteorologist. Earlier they only had an API for Harmonie weather model, which might under some special circumstances be more inaccurate than the one edited by a human expert who can combine their expertise to the the different models.

  1. Bugfix affecting the calculation of the distribution tariff, when using the seasonal pricing model.

  2. Bugfix: Change log level from ERROR to WARN when datetime is not found in the prices array, reported by @cd-tronic a couple of comments above.

Update instructions

  • Take a backup of your automation/js/node_modules/openhab-spot-price-optimizer/config.js (and other files in the node_modules directory if you have edited them manually without npm)
  • Run npm update in automation/js directory
  • Restore your previous config.js
  • Open the Rule for fetching the weather forecast and re-save it so that openHAB javascript cache gets the new version
  • Open the Rule which calculates the distribution tariff and re-save it so that openHAB javascript cache gets the new version