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

FYI:

It’ll be streamed 23/02/04 but you can already download video in the attachment…

 

I’m also commercially selling this system (see link to a demo link in link above).

And I just got a Tibber tariff myself starting in march (or both of us to develop this system, actually) so we’re now looking to combine our EMS with variable tariffing by then.

Stay tuned :slight_smile:

On a sidenote, as you need to tell your heat pump to run during cheap times you need to have some interface into it.
If you don’t happen to have an OH binding for your heat pump like the OP, use the Smart Grid ready inputs, all new and even most older pumps will have one.

I’ve just deployed a 15€ Shelly Uni for that purpose. It has WiFi and 2 potential free outputs.
If your SGr terminals are 230VAC, you need to decouple and add a another dual relay, 10€ on *bay.

Suurkiitos Markus!

I spotted your solution on the Fissio forum, and decided to give this a go. I am running Openhab on Armbian on an Orangepi 3 LTS. I would still like a walk through of how you made the nice Graphana UI work.

Timo

@timo12357 thanks for spotting the typo. I’ll fix it later today . I might have time later today to elaborate on the UI as well.

1 Like

Actually my heat pump (Nibe F-1226) does not have an OpenHab Binding. I was using the external inputs that my heat pump has, those are the exact ones that could be used to connect it to Smart Grid but at least in Nibe you can choose from many options what you want these inputs to do. For example “prevent compressor” or “prevent tap water heating”.

@timo12357 The typo is now fixed but it’s getting late so I didn’t have time for the UI write-up.

I checked my old notes and here are the tutorials / docs that you might find useful. openHabian OS would have Grafana pre-installed but I understood you are not using it.

Setting this up:

• Remember to set the Authorization header in Grafana data sourcen settings

When it comes to the actual graphs, here are a couple of queries that you might find useful:

Consumption vs. spot prices

I fetch the consumption data from Caruna with GitHub - kimmolinna/pycaruna: Caruna API for Python and store it InfluxDB as “caruna_consumption” measrument.

Queries in plain text:

from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop:v.timeRangeStop)
  |> filter(fn: (r) =>
      r._measurement == "caruna_consumption" and 
      r._field == "value"
      )
  |> map(fn: (r) => ({ _value:r._value, _time:r._time, _field:"Sähkön kulutus (kWh)" }))

And the second one:

from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop:v.timeRangeStop)
  |> filter(fn: (r) =>
      r._measurement == "spot_price" and
      r._field == "value"
      )
  |> map(fn: (r) => ({ _value:r._value, _time:r._time, _field:"Sähkön tuntihinta (c/kWh)" }))

Spot prices and the control values


The spot prices you already have above, it’s just visualized as a bar chart here but the query is the same

Control values that are rendered below:

from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop:v.timeRangeStop)
  |> filter(fn: (r) =>
      r._measurement == "nibe_control" and 
      r._field == "value"
      )
  |> map(fn: (r) => ({ _value:r._value, _time:r._time, _field:"Maalämpöpumpun kompressori" }))

Average price of the day:

You need to change the Grafana “visualization” as a “Stat”

from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop:v.timeRangeStop)
  |> filter(fn: (r) =>
      r._measurement == "spot_price" and 
      r._field == "value"
      )
  |>mean()

Table of spot prices:
Same query as for the line / bar chart, but change the “visualization” as “Table”

Embedding the graphs to OpenHab pages
See the link in the bottom of comment #13 on how to create pages in OpenHab and how to embedd the Grafana dashboards as an “Web Frame” (which means an iframe in practice, if you are familiar with iframes)

And one for the road: Here’s the dashboard I use to monitor my consumption vs. the monthly average. Bars pointing downwards mean that we have consumed on an hour that was cheaper than the month’s average and bar pointing upwards mean that we have consumed on an hour that was more expensive than the month’s average.

Query for average price, visualize as “Stat”

from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => (r["_measurement"] == "spot_price"))
	|> filter(fn: (r) => (r["_field"] == "value"))
	|> mean()

Query for consumption factor (kulutusvaikutus), visualize as “Stat”

avg_price =from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => (r["_measurement"] == "spot_price"))
	|> filter(fn: (r) => (r["_field"] == "value"))
	|> mean()
  |> findColumn(fn: (key) => key._measurement == "spot_price", column: "_value") 

total_consumption =from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => (r["_measurement"] == "caruna_consumption"))
	|> filter(fn: (r) => (r["_field"] == "value"))
	|> sum()
  |> findColumn(fn: (key) => key._measurement == "caruna_consumption", column: "_value")

caruna_consumption = from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "caruna_consumption" and r["_field"] == "value")

spot_price = from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "spot_price" and r["_field"] == "value")

join(tables:{caruna_consumption:caruna_consumption, spot_price:spot_price}, on:["_time"])
  |> map(fn:(r) => ({
      time: r._time,
      _value: ((r._value_spot_price - avg_price[0]) * r._value_caruna_consumption) / total_consumption[0], 
    })
  )
  |> sum()

Bar chart of the consumption factor, visualize as bar chart time series.

avg_price =from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => (r["_measurement"] == "spot_price"))
	|> filter(fn: (r) => (r["_field"] == "value"))
	|> mean()
  |> findColumn(fn: (key) => key._measurement == "spot_price", column: "_value") 

total_consumption =from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => (r["_measurement"] == "caruna_consumption"))
	|> filter(fn: (r) => (r["_field"] == "value"))
	|> sum()
  |> findColumn(fn: (key) => key._measurement == "caruna_consumption", column: "_value")

caruna_consumption = from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "caruna_consumption" and r["_field"] == "value")

spot_price = from(bucket: "openhab")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "spot_price" and r["_field"] == "value")

join(tables:{caruna_consumption:caruna_consumption, spot_price:spot_price}, on:["_time"])
  |> map(fn:(r) => ({
      time: r._time,
      Kulutusvaikutus: ((r._value_spot_price - avg_price[0]) * r._value_caruna_consumption) / total_consumption[0], 
    })
)
1 Like

@timo12357 The rest of the UI is now documented in the comment #13. Some of the things require quite a bit of self-learning, but I’ve provided good documentation pointers. Have fun!

1 Like

You are right, I am using an Orangepi3 LTS as platform. Grafana installs fine and I am able to configure it, but the Influx integration is a PITA. I am not able to add new users to Influx2 even when I use the all access token. Web UI has no way to add users. This may take a while to figure out as there are some references to the same problem on the web but no solutions, sic.

One thought for consideration on the implementation. Older ground source heat pumps use on-off control on the compressor, newer ones never turn the compressor off, they just turn the motor speed down. This on-off method seems to be the root cause for most ground source heat pump failures. One way to prolong the life expectancy of an old heat pump is to force it to run fewer but longer periods rather than a lot of short bursts. In practice this would mean an alternative optimization goal for price optimization: Minimize number of starts per day while keeping cost at minimum. One potential way of implementing this would be to allow only one start per period. When the length of the period is calculated the algorithm would look for the cheapest continous time within the period chosen and use that for allowing the pump to run.
I know you are using inverted steering: preventing the pump from running when energy cost is high. I use direct steering: forcing the pump to run when energy is cheap, no matter what. This latter steering approach may lend itself better for longer pump runs during the period?
If the target is cost optimizing then both investment costs, i.e. cost of compressor renovation and energy cost would be good to consider.
Just my thoughts while I wait for OpenHab to trigger the heatpump. I just installed a wifi relay controlled by your scripts into my Nibe 1245.

I remember using the influx command line tool (Influx CLI), see Command line tools for managing InfluxDB | InfluxDB OSS v2 Documentation If you need more support on that, a better place for requesting support is the Influx community forum.

1 Like

My Nibe F-1226 is not an inverter pump, it’s an on/off compressor. In practice the approach presented in this tutorial has been more than sufficient to minimize the compressor starts because most of the time we heat the house only during the night (number of heating slices equals to 1) and it’s one long heating run.

Of course if it’s very cold, we need some heating periods during the day as well. If my memory serves me well, we have had something like 2.3 compressor starts per day.

As you can see from the UI screenshots, we also have a water pump for circulating the hot tap water. It used to run 24/7 in the past, which caused a huge number of compressor starts because Nibe was thinking it needs to heat more hot water (I had not even realized the number of compressor starts it caused). Now that I’m heating the hot water at one go for the whole next day (and I have scheduled the tap water pump to run only a couple of times per day when it actually makes sense), the number of compressor starts has decreased dramatically.

According to Nibe logger, the average hours for heating the house was 6:12h per day in November and hot tap water for 0:49h per day. Corresponding figures for December were 8:45 h / day and 1:01h / day.

Controlling a heatpump that doesn’t have control inputs

In our house we have been running Markus control system slightly modified for several months now successfully. The following issues created the need for modifications in our system,

  • we have a 1 900L buffer storage for the floor heating water and a Ouman floor circuit controller

  • our heat pump has no control inputs

  • our water heater is controlled by a Fronius Ohmpilot device that also controls use of solar surplus power for water heating so it can’t be totally cut off from power

In order to get the system working we made the following modifications,

As the buffer storage supplies heat for the next 24 hours we wanted the heatpump to run continuously during the cheap night hours and only occasionally during the day if there is a cheap price hour and heating is needed. The continuous night heating is controlled by a RasPi relay we call BOOST and the occasional daytime heating by a RasPi relay we call BLOCK.

Normally the heatpump is controlled by the buffer storage and outside temperature. The outside temperature is measured with a 10k NTC resistor. When the BOOST relay is activated a 178k resistor is put in series with the NTC resistor and the heatpump senses a -32C outside temperature and heating must occure if the buffer storage temperature is below the heat curve setting. And when the BLOCK relay is activated a 22k resistor is put in parallel with the NTC resistor and the heatpump senses a +18C outside temperature and no heating is needed. If both relays are inactive the heatpump NTC resistor works normally.

We have modified the heatpump ON hours dependency of outside temperature to be a little bit logarithmic as the heat loss increases when it gets colder. The heatpump running hours effects how hot the buffer storage gets and the Ouman controller controls the floor circuit temperature according to the outside temperature.

Our water heater is controlled in the same way by a third RasPi relay that puts a 150ohm resistor in series with the original PT1000 sensor. When the relay is active the water heater is blocked from heating up between 0 - 8 am exept for the 2 cheapest hours. Between 8 am -0 it can be heated with solar surplus power.

One additional benefit with our (although forced by limitations) approach is that you don’t need to modify any mains voltage circuits.

We also have a solar panel system with a battery that we during this winter have charged with cheap night electricity and discharge during the day. When solar energy gradually will be available the night charging must be gradually lowered. We have already a script that fetches the next day solar forecast from FMI. What we still have to do is to create a script that calculates night charging depending on solar forecast and current battery charge state.

Hopefully this short information is of use for someone with similar plans…

1 Like

Out of interest, how much under the average spot price have you been able to dump your consumption @ruxu

I am using this WIFI relay: https://www.aliexpress.com/item/32845077134.html

with Tasmota firmware to control my Nibe 1245 heat pump through the pump AUX interface. I am also reading the pump status with one of the GPIOs on ESP01s unit of the relay. Just got the reading to work after a lot of trial and error. Difficult!

However, it seems the Tasmota relay is not fully obedient to Markus’ code as it sometimes turns the pump on without apparent reason. One alternative is that there is some kind of feedback loop from the ESP-01s GPIO.

Using a Wifi relay has benefits, as it is easy to fit inside the heat pump enclosure and it only handles DC 3,3 Volt connections which is a lot safer than having to deal with AC. Tasmota is supposed to be very stable, but that remains to be seen.

Here is the data for October, November and December 2022.

Average SPOT       14,07   24,22   27,06
Our average        11,46   21,59   24,39
Below avg. SPOT    -2,61   -2,63   -2,67

I think that one thing that drops our values compared to yours is that when we run the buffer storage to between 40 - 50C it drops the heat pump COP to around 4. Earlier in 2022 when the heat pump was running as needed and the buffer tank was set to 38C the COP was around 4,7.

This means that the heat pump has to run totally a little bit longer than with a better COP. And when it is requested to run in one cycle there will be some hours that are not that cheap.

@timo12357 the type of relay is not relevant here, I’m also only dealing with 3.3V when controlling my Nibe, as illustrated in the conceptual image in #13.

In other words, my relay just connects pins 3 and 4 to block the compressor. Just like your wifi relay does.

The high voltage connections I warned are for the 300 liter water heater, which is a completely separate device.

Having run this magnificent piece of software for a couple of days a few questions have popped up.

  1. I am running the system inverted compared to you, i.e. the software tells when the heat pump SHALL run, not when it SHALL NOT run. I assume this would mean I need to invert the relay control, right? Where and how would you suggest to do that?

  2. What are the recommended times to run the rules in Finland? You mention FetchSpotPrices should be run at 14:00 and 15:00. How about the ControlHeatPump and FetchWeatherForecast? What is your recommendation?

  3. Referring to our discussion about the number of compressor starts affecting the life expectancy of a heat pump, can the heat pump control be made working like the water heater code, e.g. finding the window of cheapest adjacent hours instead of just finding the cheapest hours? I am referring to your comment #68 in this thread.

Thanks for the feedback :smiley:

I have a good reason to run it this way, and that reason is that if my Raspberry is offline, then I don’t want my house to freeze. Even if you’re using wifi relays, they can be not responding as well, so I’d highly recommend to reconsider what you wish for, because you might get exactly that.

I’m running the FetchSpotPrices rule at 14.30 and 15.30 EET/EEST. Nordpool publishes the prices at 12.45 CET/CEST = 13.45 EET/EEST and I’ve learnt by experience that most of the time the prices are available at Entso at 14.30 EET/EEST. There have been a couple of days during the past 6 months when they have not been there at 14.30 EET/EEST but have been there at 15.30.

My FetchSpotPrices rule looks like this:

image

The first action is the one that calls EntsoE and fetches the spot prices

The second action is “run rule”, which is the “Determine water heating controls” rule. I also run that rule (read: re-calculate the controls) on other triggers such as changing the number of heating hours in my UI, so that’s why I don’t have it directly as “execute script”

The third action is “run rule”, which is the “Determine compressor controls” rule. Same thing here, I sometimes want to modify the nibe controls so I’ve de-coupled it as a separate rule that I’m calling here.

Thanks for the work you do on the solution!
Here is the heating plan for the coming 24h:

I have split the day into three slices. Does that lead to this kind of fractioned heating pattern where in some cases expensive hours get selected in favor of cheaper hours?

You seem to have around 10 hours per day, if I quickly calculated correctly.

Splitting the day into 3 slices mean that the slices are as follows:

  • Slice 1 = hours starting at 00, 01, …, 07
  • Slice 2 = hours starting at 08, 09, …, 15
  • Slice 3 = hours starting at 16, 17, …, 23

If you have defined that each slice must have at minimum 10% of the heating hours, it means that each of the three slices must have at least one hour (10% of 10 hours = 1 hour). If my memory serves me well, this is rounded up so that each slice will be guaranteed to have at least 1 hour.

If you have defined that each slice must have at minimum 20% of the heating hours, it means that each of the three slices are guaranteed to have 20% of 10h = 2h.

To get a grip how this behaves, experiment with it, you can run the rule manually with different parameters. Try what happens if you only use 1 slice. Then with 2 slices.

For the Jan 25th, I’m allowing 9 hours of heating in just 1 slice, which means that the compressor will be allowed to run from the 00:00 hour and it will be blocked at 09:00.

On Jan 24th I allowed 10 hours, with a result that the compressor was allowed between 00:00 and 08:00, which is 8 hours. Two hours were scheduled for the hours of 22:00 and 23:00. So the compressor will run continuously from Jan 24 at 22:00 until Jan 25 at 09:00, which is 11 hours.

The need to use the slicing depends on how well your house can keep the warm and how much big temperature drop towards the evening you and your family are comfortable with. We have a fireplace so we’ve been quite aggressive with the heat pump optimization and if it starts to feel cold in the evening, we have used the fireplace to get a cozy feeling.

Markus