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

I am not an algorithmic expert, concept which seem close to what you describe is a “sliding window” (in context of time series) and “fuzzy logic”. First because calculations you make need to be accurate according to information which is available at the execution time (time window), which is in continuous move. Second term because there is a whole set of “but” conditions which make calculation far from 0 or 1 which make decision be somewhere between 0 and 1. :wink:

Sliding window it is. But frankly I don’t understand what you mean by fuzzy logic in this context.
Provided I have the pricing data, I can determine at any time which n of the next 24 hours are the most expensive ones and if <now> is one of them then act accordingly.
Or generalised, to compensate for the fact that window size changes because new prices are obtained in 24h chunks only so that I don’t always know the complete next 24 hours, if I have values for the next k hours, I can select the (n + k)/24 most expensive ones and still determine if <now> is one of them.

At the next tick of clock with pricing data for next slot let say 24th hour since now you can find a new minimum. Also by next tick of clock a weather forecast might bring low temperatures earlier than cheapest time is, hence you need to seek for optimum. If you know that you will not keep comfort cause cheap time is too far you need to pick up a mid-ground to keep place warm, but at the time you reach mid-ground now is moved ahead and minimum might be again further than expected.

Looking at the problem as 24 slots for each hour there will be always a minimum within it with cheapest energy, so you always have true/false. Yet, if you reach situation where you have 2 or 3 continuous hours in a row with the same spot price, which one is best? Will it be always first or last? It depends on the context and other conditions which you can clearly score.

I’m not sure how likely an ever-falling price time series is that could result in too many ‘off’ hours so we cool down ever waiting for things to become even cheaper and cheaper (like deflation in money theory).
To play it safe I think we need to track in how many of the past 24h we already stopped heating and use that as a “but” condition in your terms that overrides if the sliding window’s computation concludes we should not heat now because we see falling prices ahead so it would be cheaper to wait even more. But as said not sure if that’s worth checking/preventing.

I recognize the phenomenom. It’s somewhat related to the cold winter days here in the North where we could have cheaper hours coming in the evening but to prevent too big cooling, we need to allow some heating during the day. And when this is needed, it makes sense to find the local minimum from the daytime.

So this comes back to the fact that the optimization problem has multiple objectives: comfort and price optimization.

Coming back to the concrete topics and questions.

We know as a fact that Nordpool publishes the day-ahead prices at 12.45 CET/CEST. We also know that the data is usually available at API endpoints like Entso-E at 13.15 CET/CEST. Thus, the price fetching is typically scheduled to occur in the afternoon. The calculations can conceptually be modified somewhat trivially so that it does the optimizations from “now” until the end of the price window.

For the “prevent too much cooling” dilemma, this can be addressed in different ways.

One is of course to use a less intrusive control strategy where the device can work with its own internal logic which is usually based on feedback loop from temperature sensors. This optimizes the comfort in favor of price.

A second approach could be to define a number which defines the max not-allowed heating period and then find local minimums from prices.

A third one is almost the same i.e. the slicing concept of my solution that we have discussed already many times in this thread.

I’m sure combinations of these exist. If for example we can make a safe assumption that we have reliable indoor temperature measurements, then this “let’s kick in the prevention of too much cooling” logic could be wired to the current indoor temperature, current outdoor temperature, weather forecast (temperature and cloudiness being significant factors, wind speed to some extent) and spot prices. It’s a balance between complexity and the optimization objectives.

We should not assume there’s indoor temp measurements available for the average user
If someone has it they can of course additionally deploy it to complement whatever we do in the algorithm but in many environments it just isn’t there and we need to provide a solution that works for everyone.
So your #1 isn’t applicable. Not sure how your #3 slicing proposal would solve this so I’m with your #2.
I wonder what’s the best ‘but’ condition: do we need the user to define a maximum not-allowed period or can we auto-derive that from the slice size or from the ‘n’ of my example which actually isn’t a period but the number of hours we’re allowed to “not-heat” ?

I agree with you that the assumption that the indoor temperature is (reliably) available is not safe. For example, I have a DS18B20 sensor in our living room but it’s only “for info” purposes. I would most definitely not use it for control purposes.

In the slicing concept: Let’s assume that the number of needed heating hours is known, let’s use 12 as an example.

If the day is split into 2 x 12h parts and both parts are guaranteed to have 40% of the heating time, that would mean that both 12h parts have at least 5 hours, if we round up. In the worst case, the first part would have 7 h in the beginning and the latter would have 5 in the end. Meaning that the max cooling period on a total 24h window would be 12h. So not that much of a benefit.

If the 24 hour window is split into 3 x 8 hour parts and each part is guaranteed 30% of the heating time, it would mean that each part has 4 hours if we round up. That would mean that in the worst case, the first part has all 4 hours in the beginning and the second part would have them at the end. Meaning that the max cooling period is 8h.

If the day is split into 4 x 6 hour parts and 20% of heating time is guaranteed for each part, it would be 2 or 3 hours per part depending how we do the rounding. Let’s say that 2h would be guaranteed for each part. Max cooling period in the worst case is again 8h if the first part has the 2 hours in the beginning and the next one has them at the end.

The question between the approaches 2-3 is that do we use absolute number of max cooling hours in the algorithm or do we use relative / percentage values. The goal and result is the same, in other words to guarantee / force a more even distribution of the heating time.

@mstormi some Nibe models have a feature called Smart Price Adaption. It’s documentation is next to non-existing and my pump does not have this feature so I haven’t been able to test in practice how it works. According to the Finnish forum posts (which are very biased to enthusiasts and do not represents average users) it’s not very aggressive in terms of cost optimization. According to some users it’s not for example scheduling the heating of domestic hot water to the cheapest hours which is kinda “interesting”.

Anyway, one thing that we might find inspiring is that it has a settings where the user can choose how much weight is given to cost optimization vs. comfort. This kind of slider could be translated to this “even distribution of heating time” / finding local minimums within the price window.

I just checked my electricity bill 1-31.3.2023

Consumption: 1201,96 kWh
Average spot price paid: 7,26 c/kWh
Energy bill total: 90,81€ including tax and 0,22c/kWh margin

At the same time our local energy company was providing one of the cheapest fixed energy prices in the country at 16 c/kWh. 1201,96 kWh x 0,16€/kWh = 192,31€

Total savings in March 101,5 €! I think this is proof enough that the code works.

Thank you for the hours you have put in developing the code, publishing it and answering our questions here in the forum, @masipila!

7.26 c/kWh as an average is quite nice achievement. The market average on March was 8.16 so you were 0.9 c/kWh below it i.e. -11% compared to market average.

Without active scheduling you would have most probably been at least +25% because daytime prices are most of the time above monthly average.

Our March was -2.4 c/kWh (-27%) compared to market average. Because of electric vehicle our consumption was a bit higher than yours, about 1720 kWh.

Now that OL3 is finally up and running the prices are not jumping that much anymore. In April so far we are -1.8 c/kWh and the average so far is 6.85. So percentage wise still on the same ballpark i.e. between 25-30%

For non-Finns: OL3 is the notorious 1600 MW nuclear power plant which finally started commercial production last weekend, only 14 years delayed compared to the original schedule. According to Wikipedia, it’s the third most expensive building ever built in the World.

Including passive solar heat.
The average temperature here in the arctics has for the past two weeks been between +5 to +10 C with clear skies and a lot of sunshine. Our house has big windows to the south and west that let in a lot of sunshine and provide additional heating. Today the weather changed, we are down to 0 C and 100% overcast. As a results the straight heating curve provided by the code no longer serves it purpose right but the house cools down. I have manually changed the curve few times when this happens, but I think the influence of passive solar heating could be automated, too.

Reading the windchill code I understand it takes the temperature forecast and wind forecast and calculates a windchill corrected temperature value for each hour of the temperature forecast. The original temperature forecast is then replaced hour by hour with the corrected one. Heating hours are calculated based upon the average of these corrected temperature forecast hours. Correct @masipila ?

To include the passive solar heat a couple of factors should be considered:

  1. Time of the year. Up here in the north the sun does not provide significant heat from beginning of November to end of February. During this time passive solar heating should not be included in the heating hours.

  2. Time of day. Solar heat is typically available only if the sun is above the horizon ;-). This could be approximated e.g. by taking the passive solar effect into account only between 11:00 and 18:00 local time. The local sunrise and set times can also be downloaded from a internet source such as Sunrise and Sunset Calculator and if a more exact calculation is desired.

  3. Cloudiness. The FMI forecast gives a hourly cloudiness percentage that can be used to calculate average cloudiness over the time period when the sun is up. This can be used to compensate for a weather situation like the one we have right now.

I am not quite sure how the algorithm should work. Should it recalculate the hourly forecast temperatures like the windchill algorithm seems to do or should it just add and subtract from the heating hours calculated. Opinions?

As I am no coder I am not able to write the code needed, even with the help of ChatGPT, to solve this, so help is needed. But before that, I would like to hear your opinions on my ideas of including passive solar heating in the code.

Almost correct, except that I store both the “raw” temperature forecast as “fmi_forecast_temperature” and in addition to this, I store “fmi_forecast_WindChillTemp”. So everything you wrote is correctly understood except that I don’t “replace” the temperature forecast, I store both. And then, in the scripts that calculate how much heating is needed, I use “fmi_forecast_WindChillTemp”, not “fmi_forecast_temperature”.

You might want to consider de-coupling the “calculate the number of needed heating hours” into a separate rule. The output of this would then go to a separate Item, called for example “number of heating hours”. You can expose this Item in the user interface like I do, like this:

Basically what happens here is that I have a rule “Calculate Heating Hours” that runs every day at 14.20. The result of this calculation is saved to Item called “HeatingHours”. I have a separate Page called “Ohjausparametrit” (control parameters in Finnish) where I can manually adjust the number of heating hours if I’m not happy with the automatically calculated number for whatever reason. I implemented this in the winter time because there were a couple of days when the FMI weather forecast provided by their API was like 10 degrees off from what their meteorologists were saying on TV, on their website and their mobile app.

On this “Ohjausparametrit” page, I also have similar Item for selecting how many hours the water heater needs (it’s usually set to 2 and I change it only if I have a reason for it). And I have an Item for selecting how many hours our electric vehicle needs to be charged. We update this number manually with this stepper UI widget and it’s quite handy since 10% increase of battery charge level corresponds almost exactly 1 hour of charging time. So if we need to charge the car from 50% to 80%, we would check that the value of this “CarChargingHours” is 3.

The point here is that when this “calculate heating hours” is a Rule of its own, you can make it as sophisticated as you want. For example, if it’s March - October, apply this logic and otherwise, apply this logic.

Finally, you should apply a logic that WHEN the value of HeatingHours changed, THEN trigger the other rule that optimizes your heating hour control points.

openHab is your best friend because most of things that you need have already been solved by somebody else. See the Astro Binding, Astro - Bindings | openHAB


  • You want to have a separate Rule that calculates the number of needed heating hours.
  • This Rule updates the Item called HeatingHours
  • When HeatingHours changes, (re-) trigger the heating optimization rule and feed the current value of HeatingHours as the input to the heating optimization.
1 Like


Would this mean to splice out this part of nibe.js, like so?

**  GNU nano 5.4                            /etc/openhab/automation/js/node_modules/sienitie18/nibe.js                              >
 * Javascript module for Nibe helper methods.
 * Copyright (c) 2022 Markus Sipilä.
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.

 * Exports.
module.exports = {
    calculateNumberOfHours: calculateNumberOfHours
 * Calculates number of needed hours for given average temperature.
 * @param float temperature
 *   Average temperateure for the day.
 * @return float
 *   Number of hours the heat pump should be allowed to run.
function calculateNumberOfHours(temperature) {
    console.log('nibe.js: Calculating number of ON hours for Nibe...');

    // Early exit if temperature is null.
    if (temperature == null) {
        console.warn('nibe.js: No temperature given! Number of needed hours defaulted to 24!');
        return 24;

    // Calculate curve based on two constant points.
    // y = kx + b
    // x = temperature, y = number of needed hours.
    const p1 = {
        x : -27,
        y : 16
    const p2 = {
        x: 2,
        y: 6
    const k = (p1.y-p2.y) / (p1.x-p2.x);
    const b = p2.y - (k * p2.x);
    console.debug('nibe.js: y = ' + k + 'x + ' + b);

    let y = k * temperature + b;
    if (temperature < p1.x) {
        y = p1.y;
    if (temperature > p2.x) {
        y = p2.y;
    console.log('nibe.js: Number of needed hours: ' + y);
    return y;

You don’t need to modify the javascript files. You currently have the Rule where you

  1. determine the number of heating hours and
  2. pass that number to the method that calculates the ‘nibe_control’ control points for your heat pump.

First, you need to create a new Item called for example HeatingHours.

Once you have this, create a new Rule called for example DetermineHeatingHours. This rule will have nothing to do with nibe_control. All it does, is to calculate the number of needed heating hours and save that result to the item HeatingHours.

Then, you want to modify your current rule that writes the nibe_control points so that this script no longer calculates the number of heating hours on the fly. Instead, it reads the number from your Item called HeatingHours and passes that value to the method that generates the nibe_control points.

Look for ‘solar forecast PV’ on the marketplace. This it is what I am using in my EMS.

1 Like

Thanks for the heads up, @mstormi!
Solar forecast PV was an easy way to achieve what I was looking for. After installin Solar Forecast PV I registered a free account at Solcast and created a virtual solar panel of my house in Solcast with max power 100 kW. This yields around 700 kWh max energy daily. In OpenHAB I created a number item that reflects the max forecast energy for tomorrow. I divide the forecast with the max energy, i.e. 700 and thereby get a coefficient by which I multiply the max reduction hours from the heating time.

My script is run at 20:00 daily and it corrects the heating plan for next day based upon incoming solar energy. Here is my script:

console.log("Item heatingHours before solar inluded", items.getItem("heatingHours").state);

//Get energy forecast from Solcast
solarEnergy = items.getItem("Solcast_PV_Plane_Forecast_Tomorrow").state;

// calculate reduction coeffficient
q = solarEnergy/700;
console.log("Reduction coefficient: ", q);

// maximum heating hours reduction thanks to solar [h]
k = 1;

// calculate heating hours correction using the reduction coefficient
m = q*k;
n = items.getItem("heatingHours").state - m;
console.log("Item heatingHours after solar included",items.getItem("heatingHours").state-m);

// update heating hours

I spotted this graph from the latest issue of Tekniikka & Talous

It supports your considerations that we don’t get any significant amount of passive heating between the beginning of November and end of February.


Should we start a sepate thread for savings discussion?

Just got my electricity bill for April 2023:

Consumption 854 kWh
Average cost 5,73 c/kWh
Margin 0,22 c/kWh
Monthly fee 4,36 €
Total bill 55,15€

Local energy company
Cost of energy 13,18 c/kWh
Monthly fee 2,5 €
Total cost would have been 115,06
Saved: 59,9€

Average spot price
April 6,661 c/kWh
Difference: 0,93 c/kWh = 14%
Saved: 7,95€


That’s a great idea, please go ahead and create a thread for it and mentioned me and I’ll chime in!

Is it possible with OH scripting to make k dependent on the month, like so


if currentMonth == (march or october) then k = 0,5
else if currentMonth == (april or september) then k = 1
else if currentMonth == (may or august) then k = 2
else if currentMonth == (june or july) then k = 3
else k = 0

or does this require external scripts and separate rules for each of the alternatives?