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

Beside the program structure:
After taking a look at your graph, have you considered, taking (outside) temperature into account? Electricity might be cheapest in the middle of the night, but it’s also the coldest hours of the day, so your heat pump runs least efficient. So overall cost could be less, running it in the afternoon/evening with slightly higher electricity costs but much better efficiency.
By using weather forecast, you could also calculate this in advance.

@AlexW the ground source heat pump will be the next phase of the project after I’m done with the water heater. And yes, the outside temperature definitely needs to be taken into account in that.

This is how I see things at this point of time…

Hot water
The hot water is first heated by the heat pump and is in the 180 liter integrated water tank. This is done very efficiently with the ground source heat pump. However, the 180 liters is not enough for our house because we have a bathtub. For that reason, we have that external water heater (300 liters) that is “behind” or “after” the 180 liters integrated water tank. That is heated simply with electricity.

I want to first control the 300 liter water heater so that I avoid a situation where I would cut off the heat pump during expensive hours but then the 300 liter water heater would be turned on as a consequence. So this is going to be phase 1 of the project, and that is conceptually quite straight forward as described above; I only need to find the 4h period which is cheapest and the water heater will heat the water to the thermostat max temperature.

Controlling the ground source heat pump
Once the 300 liter water heater is sorted out, it’s time to focus on the heat pump. In Finland the outside temperature during the coldest winter can be -25 Celsius or even colder. So during this kind of a time, I can probably only disallow the single most expensive hour of the day. Or it might be that I’ll be able to disallow 2 or 3 most expensive hours as long as they are not adjacent hours. Time will tell next winter.

However, during the summer months, the house does not heating at all, it’s actually the opposite that we need to cool the house with air source heat pumps. The only thing that the ground source heat pump is doing during the summer months is heating the hot water (which then goes to the 300 liter water heater before it is delivered to the taps and showers). During these kind of outdoor temperatures I can easily have similar logic that I have with the water heater, where I basically find some cheap hours and allow the compressor to run during those hours.

I need to experiment this a bit but my work hypothesis is that the ground source heat pump will be something like this:

  • when it’s -20 Celsius, I need to allow the ground source heat pump to run 22 hours of the day (I can block 2 most expensive hours)
  • when it’s 0 Celsius, I need to allow the the ground source heat pump to run 10 hours of the day
  • when it’s +20 Celsius, I only need to allow the ground source heat pump to run 2 hours of the day

Other temperatures would obviously be somewhere in between these thresholds.

Charging the electric car
A third factor will be the charging of an electric car which hopefully will be eventually delivered this fall (I ordered it last October). I haven’t done the math yet, but then I need to see which of the three big loads (car charging, heat pump, water heater) can be on at the same time. The car charging will be 11kW, ground source heat pump is 8kW and water heater 3kW. Our main fuse is 3x35A.

I’m also trying to achive some similar stuff like you do. I’m having a Nibe geothermal heatpump that I prevent electrical boiler from running if powerconsumption is high.

For this winter I’m trying to add on a economic state for all my high consumptions thta is Nibe heatpump, and Easee car charger, and two pools, all this is firts of all gonna fit on 3x25A main fuses, and primarly also run while spotprice is low.

I did steal your example and did get spotprices from API and store all spotprices in a string item containing the whole Json result from xslt to json transforming.

However your printout for eventlog is off by 2 hours for me, first hour is 23 and last hour is 21 meaning 1 hour is missing. And the I need to or at leats like to convert UTC time to loacaltime (Sweden).

If you setup every hot as a Number Item and join them to a minimugroup, you will allways habe the hours sorted from cheapest hour to most expensive hour, purly baed on cost rate.

PS Eassee charging box is highly recommended, worsk like a charm with the built in simcard, my box is controlled via Openhab.

I fill up wit a little progress, stored start date time into DateTime item as below. Time stores nicely and sitemaps convert it to local time, now I just need to figure out how to make comparsions with UTC time to local Time or just store same value as localtime into a item.

SpotpriceStartDateTimeUTC.postUpdate(transform("JSONPATH", "$['Publication_MarketDocument']['period.timeInterval']['start']", priceJson));

I decided to refactor my scripts to Javascript so that I can reuse the code in different places. I hope to be able to share my rules and where I am either today or tomorrow.

What’s working so far, but I’m currently working to refactor the code:

  1. Fetch spot prices from Entso-E API and store them as future-timestampped points to InfluxDB

  2. Read the stored spot prices from InfluxDB and calculate the on/off hours for the waterheater based on that. These on/off hours are stored as different future-timestampped points to InfluxDB

  3. An hourly rule that runs every full hour that checks if the waterheater should be turned on/off and controls the GPIO output. I currently have a simple buzzer connected to the GPIO but I should get my father-in-law (an electrician) to do the relay & contactor connections in the next week or so.

1 Like

Update on Feb 22, 2024.

The solution discussed in this thread evolved to a NPM package openhab-spot-price-optimizer.

The blue area in the figure above represents the spot prices. The yellow bars indicate when the heating of the house will be ON and the red bars indicate when the water boiler will be ON to heat domestic hot water. The solution can be used with any devices you can control with openHAB.

Installation instructions and full documentation with examples can be found at Home · masipila/openhab-spot-price-optimizer Wiki · GitHub

The comments between this and comment #302 are related to the older version of the solution. Comments from #302 onwards are related to openhab-spot-price-optimizer linked above.

12 Likes

Thanks all who helped and pushed me to the right direction along the way!

If you have any suggestions to improve the code, I’m all ears.

And one more disclaimer: I promised myself 21 years ago that I will never ever touch javascript again and this is now the first time I broke that promise (I have to admit it was not that bad after all.) But having said this, this is literally the first time in more than 20 years that I’m writing JS so it might be that I’m not following some conventions properly…

I edited my own comment #13 (which has been marked as the solution) so that it now also includes the control logic for my Nibe ground source heat pump.

  • I read the weather forecast from the Finnish Meteorology Institute’s API and store the temperatures for each hour to Influx database.
  • Based on the average temperature for the day, I will determine how many hours the heat pump compressor must be allowed to run. When it’s -20 celsius, 22 hours will be required (I can avoid 2 most expensive hours). When it’s +20 celsius, 2 hours will be required for heating the hot water. This heat curve is configurable in the code.
  • Once the number of hours is known, I’ll determine the control points as “nibe_control” measurement points.
  • To avoid a situation during the winter where the heat pump is on for say 12 hours in a row and then off for the next 12 hours, the day can be split to a desired amount of “slices”, for example 2 x 12 hours or 3 x 8 hours.
  • For every slice, we can define a minimum percentage of the “on” hours. Example: Let’s say 10h of heating is required and the day is split into 2 slices. We can define that at least 10% of the 10 hours must be allocated to all slices. Meaning that both slices would have 0.1 x 10h = 1 hour of heating.
  • The slices and percentages are configurable in the code.
1 Like

Looks like a really nice solution I will try to use the most of your solution, but will try to use mariadb instead of influxdb, I do not know if thats even possible but I will give it a try as soon vaccation is over and I’m at home.

1 Like

Nice that you found this useful @Marcus_Carlsson !

Regarding the MariaDB vs. InfluxDB. The key gotcha here is that you must be able to write measurement points with future timestamps.

I decided to move my database to run on a NAS server instead of the Raspberry when openHAB runs so that I can minimize the write operations to the Raspberry SD card. When I did that transition, I tried to use MariaDB (which was available on my NAS with a point-and-click configuration) but I was not able to figure out how to write points with future timestamps, see Is it possible to use the JDBC service to access MariaDB from a script

If you’re able to crack where I went to the woods, feel free to comment on that thread.

I ended up installing InfluxDB on my NAS and it’s now up an running. The first results look quite promising. The screenshot below is from my electricity company. The black line is the price and bars are my consumption. :slight_smile:

Cheers,
Markus

Hi now I am in the deed of getting this tested for my solution, and I am absolutley no professional programmer but I try to learn as we go.

I stole all your scripts and folder structure for testing, influxdb runs in a docker container on an synology nas and openhab succeccfully writes other data to it.

I did change database adress and token in influx and set token and area in entose.js

But when I create rules and launch them all rule fail with

Script execution of rule with UID ‘a8a539c0b4’ failed: ReferenceError: “require” is not defined in at line number 1

Do I need to install any extra script engine or will it run with ECAM 262 edition 5.1

Reply to myself, Edition 11 was the right one, installed JSSripting and now have edition 11, however Exeption throws on xslt transformation.

entose.js: transforming XML to JSON and parsing prices…
17:29:04.612 [ERROR] [org.openhab.automation.script ] - entsoe.js: Exception parsing spot prices: Invalid JSON: :1:176 Trailing comma is not allowed in JSON
{“Publication_MarketDocument” : {“mRID” : “eda9f974a1d9439db156d7dd57f2df6d”

However script says it succefully written mesaurments to influxdb, but I cannot find them.

It might be a problem with the xslt transformation. When I trigger the rule and use xml2json.xsl the following error occure that ´probably ends up not having any values there right now.

The value of attribute “test” associated with an element type “xsl:if” must not contain the ‘<’ character.

And I found documentation about that < was not allowed and nedded to be replace with < thats the correct operator. When I did that I do not get that error but still no data in influx.

FORECAST SCRIPT
Errors with
fmi.js: Making an API call to FMI API…
20:27:04.890 [ERROR] [org.openhab.automation.script ] - fmi.js: Temperatures parsing failed!
20:27:04.897 [INFO ] [org.openhab.automation.script ] - influx.js: Preparing to write points to the database

And fetch spotprice erros with

  • entsoe.js: Making an API call to Entso-E API…
    20:30:35.173 [INFO ] [org.openhab.automation.script ] - entose.js: transforming XML to JSON and parsing prices…
    20:30:35.283 [ERROR] [org.openhab.automation.script ] - entsoe.js: Exception parsing spot prices: Invalid JSON: :2:0 Invalid JSON: String contains control character
    " :
    ^
    20:30:35.287 [INFO ] [org.openhab.automation.script ] - influx.js: Preparing to write points to the database for spot_price

Does anyone have any idea?

Hi @Marcus_Carlsson !

The error message here means that you are not able to read the forecaster temperatures from the the Finnish Meteorology Institute’s weather forecast. You most probably do not want to read the weather forecast from Finnish Meteorology Institute anyway?

To narrow things down, you can temporarily modify the rule script so that you don’t calculate the number of hours based on the weather forecast. Instead, just put there a static value like 5 when you’re debugging this:

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

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

// Comment out these two lines that fetch the weather forecast and calculate how many hours are needed.
// t = nibe.getForecastTemp(start, stop);
// n = nibe.calculateNumberOfHours(t);
// Temporarily define the number of hours as five.
n = 5;
nibe.determineHours(start, stop, n, 3, 0.1);

You can add a couple of debug messages so that you can see from your logs what is going on.

Add the debug lines like this to get the complete XML response written to your log:

function makeApiCall(start, end, area, token) {
   [...]
    priceXml = http.sendHttpGetRequest(url, 10000);
    console.debug('Complete XML response:');
    console.debug(priceXML);
    return priceXml;

Add these debug lines to see if the XML to JSON transformation works:

function preparePoints(priceXml, tax) {
   [...]
	const prices = JSON.parse(transformation.transform('XSLT', 'xml2json.xsl', priceXml));
        console.debug('Complete JSON:');
        console.debug('prices');

These should help you to narrow down what’s going on…

Cheers,
Markus

@Marcus_Carlsson one more thing related to debugging.

If (when) you modify the javascript files, you need to “refresh” your rules so that openHAB will use the updated versions of the js files. This can be done as follows:

  • Go to edit the rule that uses the js file
  • Save the rule
  • Run the rule

If you don’t go to the edit mode and save the rule, openHAB will use the old version of the JS file it had read to memory before.

Hi

Thanks for your efforts to help me out, I managed to get some xml output after changing console.debug to console.log, but somewhere in xml2json there must be a problem, I don’t se any output from extensive console.log in xml2json function. Meybe it wasn’t right to change < to

 &lt;

In the xml2jsonfile, I downloaded an copy an pasted xml2json file again and now it works better. However influx db doesn’t contain much data spotprice came up but only one price point

2022-07-27 15:06:34.088 [INFO ] [org.openhab.automation.script       ] - entsoe.js: Making an API call to Entso-E API...
2022-07-27 15:06:34.591 [INFO ] [org.openhab.automation.script       ] - Complete XML response:
2022-07-27 15:06:34.600 [INFO ] [org.openhab.automation.script       ] - <?xml version="1.0" encoding="UTF-8"?>
<Publication_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:0">
	<mRID>5481dbea73224a0e8c4cd7ec6b1694af</mRID>
	<revisionNumber>1</revisionNumber>
	<type>A44</type>
	<sender_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</sender_MarketParticipant.mRID>
	<sender_MarketParticipant.marketRole.type>A32</sender_MarketParticipant.marketRole.type>
	<receiver_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</receiver_MarketParticipant.mRID>
	<receiver_MarketParticipant.marketRole.type>A33</receiver_MarketParticipant.marketRole.type>
	<createdDateTime>2022-07-27T13:06:34Z</createdDateTime>
	<period.timeInterval>
		<start>2022-07-27T22:00Z</start>
		<end>2022-07-28T22:00Z</end>
	</period.timeInterval>
	<TimeSeries>
		<mRID>1</mRID>
		<businessType>A62</businessType>
		<in_Domain.mRID codingScheme="A01">10Y1001A1001A46L</in_Domain.mRID>
		<out_Domain.mRID codingScheme="A01">10Y1001A1001A46L</out_Domain.mRID>
		<currency_Unit.name>EUR</currency_Unit.name>
		<price_Measure_Unit.name>MWH</price_Measure_Unit.name>
		<curveType>A01</curveType>
			<Period>
				<timeInterval>
					<start>2022-07-27T22:00Z</start>
					<end>2022-07-28T22:00Z</end>
				</timeInterval>
				<resolution>PT60M</resolution>
					<Point>
						<position>1</position>
						<price.amount>2.65</price.amount>
					</Point>
					<Point>
						<position>2</position>
						<price.amount>2.16</price.amount>
					</Point>
					<Point>
						<position>3</position>
						<price.amount>2.26</price.amount>
					</Point>
					<Point>
						<position>4</position>
						<price.amount>2.48</price.amount>
					</Point>
					<Point>
						<position>5</position>
						<price.amount>3.23</price.amount>
					</Point>
					<Point>
						<position>6</position>
						<price.amount>5.23</price.amount>
					</Point>
					<Point>
						<position>7</position>
						<price.amount>6.91</price.amount>
					</Point>
					<Point>
						<position>8</position>
						<price.amount>50.05</price.amount>
					</Point>
					<Point>
						<position>9</position>
						<price.amount>200.05</price.amount>
					</Point>
					<Point>
						<position>10</position>
						<price.amount>206.57</price.amount>
					</Point>
					<Point>
						<position>11</position>
						<price.amount>209.92</price.amount>
					</Point>
					<Point>
						<position>12</position>
						<price.amount>209.98</price.amount>
					</Point>
					<Point>
						<position>13</position>
						<price.amount>209.97</price.amount>
					</Point>
					<Point>
						<position>14</position>
						<price.amount>197.19</price.amount>
					</Point>
					<Point>
						<position>15</position>
						<price.amount>204.25</price.amount>
					</Point>
					<Point>
						<position>16</position>
						<price.amount>200.09</price.amount>
					</Point>
					<Point>
						<position>17</position>
						<price.amount>205.73</price.amount>
					</Point>
					<Point>
						<position>18</position>
						<price.amount>214.49</price.amount>
					</Point>
					<Point>
						<position>19</position>
						<price.amount>220.97</price.amount>
					</Point>
					<Point>
						<position>20</position>
						<price.amount>220.96</price.amount>
					</Point>
					<Point>
						<position>21</position>
						<price.amount>214.91</price.amount>
					</Point>
					<Point>
						<position>22</position>
						<price.amount>212.83</price.amount>
					</Point>
					<Point>
						<position>23</position>
						<price.amount>207.25</price.amount>
					</Point>
					<Point>
						<position>24</position>
						<price.amount>179.56</price.amount>
					</Point>
			</Period>
	</TimeSeries>
</Publication_MarketDocument>
2022-07-27 15:06:34.627 [INFO ] [org.openhab.automation.script       ] - entose.js: transforming XML to JSON and parsing prices...
2022-07-27 15:06:34.828 [INFO ] [org.openhab.automation.script       ] - Complete JSON:
2022-07-27 15:06:34.835 [INFO ] [org.openhab.automation.script       ] - prices[object Object]
2022-07-27 15:06:34.847 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-27T22:00:00.000Z 0.265 c/kWh
2022-07-27 15:06:34.854 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-27T23:00:00.000Z 0.21600000000000003 c/kWh
2022-07-27 15:06:34.861 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T00:00:00.000Z 0.22599999999999998 c/kWh
2022-07-27 15:06:34.868 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T01:00:00.000Z 0.248 c/kWh
2022-07-27 15:06:34.875 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T02:00:00.000Z 0.323 c/kWh
2022-07-27 15:06:34.882 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T03:00:00.000Z 0.523 c/kWh
2022-07-27 15:06:34.890 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T04:00:00.000Z 0.6910000000000001 c/kWh
2022-07-27 15:06:34.897 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T05:00:00.000Z 5.005 c/kWh
2022-07-27 15:06:34.904 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T06:00:00.000Z 20.005000000000003 c/kWh
2022-07-27 15:06:34.911 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T07:00:00.000Z 20.657 c/kWh
2022-07-27 15:06:34.918 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T08:00:00.000Z 20.991999999999997 c/kWh
2022-07-27 15:06:34.926 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T09:00:00.000Z 20.997999999999998 c/kWh
2022-07-27 15:06:34.933 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T10:00:00.000Z 20.997 c/kWh
2022-07-27 15:06:34.940 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T11:00:00.000Z 19.719 c/kWh
2022-07-27 15:06:34.947 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T12:00:00.000Z 20.425 c/kWh
2022-07-27 15:06:34.954 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T13:00:00.000Z 20.009 c/kWh
2022-07-27 15:06:34.961 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T14:00:00.000Z 20.573 c/kWh
2022-07-27 15:06:34.968 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T15:00:00.000Z 21.449 c/kWh
2022-07-27 15:06:34.976 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T16:00:00.000Z 22.097 c/kWh
2022-07-27 15:06:34.983 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T17:00:00.000Z 22.096 c/kWh
2022-07-27 15:06:34.990 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T18:00:00.000Z 21.491 c/kWh
2022-07-27 15:06:34.997 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T19:00:00.000Z 21.283 c/kWh
2022-07-27 15:06:35.004 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T20:00:00.000Z 20.725 c/kWh
2022-07-27 15:06:35.011 [INFO ] [org.openhab.automation.script       ] - entsoe.js: 2022-07-28T21:00:00.000Z 17.956 c/kWh
2022-07-27 15:06:35.017 [INFO ] [org.openhab.automation.script       ] - influx.js: Preparing to write points to the database for spot_price
2022-07-27 15:06:35.200 [INFO ] [org.openhab.automation.script       ] - influx.js: Points successfully saved for measurement spot_price

The reason why you’re not seeing more than one value is that your Influx Query is searching for data for “past 1 hour”. Change the range to include tomorrow and you’ll see tomorrow’s spot prices :slight_smile:

Thanks again for all your efforts of helping men out. I will be out travelling for next couple of days but wilöl have spotprice fetching rule up an running every hour for the moment I did indeed have more data if I set a different filer :slight_smile: .

I did get FMI forecast script to runt but it didnt write an mesurement, and I beleive that thats the reason why nibe and waterboiler script fails theres no temperature forecast. I will probably convert this to my local realtime temperature but thought I will get this nice example up and running before I start modifying to much. And I will also integrate this with my Easee EV wallbox.

The waterheater rule is not depending on the weather forecast at all. It should work if/when you have the spot prices stored in your influxDB.

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

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

wh.determineHours(start, stop, 4);

The last row means that “between the start midnight and stop midnight, find a 4 hour window when the spot prices are lowest”.

Our hot water consumption does not depend on the outside weather. Four hours should be easily enough to heat the water for the whole day.

  • 1 kWh will be required to heat 864 liters of water by 1 degree
  • My water heater has a 300 liter tank so it will require 300/864 = 0,347 kWh to raise the water temperature by 1 degree
  • My water heater is 3 kW so letting it run for 4 hours means 12 kWh
  • Which means that 12 kWh can raise the water temperature by 12/0,347 = 34,5 degrees

This is easily more than enough because the water heater is “behind” or “after” the Nibe ground source heat pump which pre-warms the water in its 180 liter integrated tank to 48 degrees so the starting point for the water heater is barely ever under 40 degrees.

I’m currently planning to install a couple of DS18B20 temperature sensors to the outer shell of the tank of the water heater so that I could measure how cold / warm the water is already and then determine if I need to allow 1, 2, 3 or 4 hours of heating time for the water heater.

1 Like

Here’s what I’ve been able to achieve so far. Blue bars is my consumption which I’m able to fetch from the power grid operator and green line are the spot prices. You can see nice consumption peaks when the prices are lowest.

July 26 and 27 were hugely exceptional days in Finland in terms of the spot prices as there was a lot of wind power and at the same time the Baltic countries had planned maintenance in their power grid which meant that they could not import electricity from Finland which they normally do. I think we washed laundry something like 4 or 5 times, cleaned the oven with pyrolysis and so on.

My electricity contract is not directly bound to the Nordpool spot prices (i.e. spot price + margin). Instead, I have a contract which is 14.8 c / kWh +/- “consumption factor”. The consumption factor is calculated as follows:

consumption factor = (A-B) / E, where

  • A is the sum of consumption * spot price over every hour of the month
  • B is the total consumption * average price in Nordpool for the month
  • E is the total consumption of the month

In other words, if I use power on hours which are cheaper than the month’s average spot price, the consumption factor will be negative and decrease the price that I pay of the whole month’s power. With these optimizations this means that the 14.8 c/kWh is effectively a ceiling price that I will have to pay under any circumstances, even if the Nordpool would be at 100c / kWh next winter.

Here’s a 7 day trend for the consumption factor. If I would be able to keep this trend, I would pay 14.8 - 5.9 c / kWh but it’s not going to be that much negative because the July 26-27 was so highly exceptional. But anyway, this I will have a double digit discount to the power bill, which is is quite nice :slight_smile: