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

Hi all,

I’m about to implement a home automation system that will control the heating system of our house so that it will avoid the expensive hours when electricity costs the most.

I have a programming background but I’m new to OpenHab and would appreciate some guidance on the Right OpenHab Way to do things.

Let me first explain what I’m planning. I’m writing this quite verbosely so that this would benefit other community members in the future.

Background and context: heating system of our house and Nordpool day ahead spot prices

The electricity price follows the spot price of Nordpool. The price / kWh is different for each hour of the day and the prices for the next day are published on the previous evening. See Market data | Nord Pool

The prices have been jumping like crazy. The cheapest hours are like 0.5 c/kWh but the most expensive hours can be 70 c/kWh or even more.

Our house is heated with a ground source heat pump. It heats up water for the underfloor heating and it has an integrated 180 liter tank for hot water. The heat pump is Nibe F-1226 8kW. The heat pump has connectors for external control so it should be quite easy to control.

In addition to the 180 liter integrated water tank in the heat pump, we have a separate 300 liter water heater (Nibe Haato 300, 3kW). This heater has a mechanical thermostat which controls when the heater is on/off.

The plan for the external water heater

The spot prices for each hour of the day can be fetched with an API provided by Entso-E Transparency Platform. Anyone can create an user account to the platform and request API access to be granted, this is free of charge. See the API user guide if you want to learn more.

My plan for the water heater is quite simple.

  • Read the spot prices for next day from Entso-E

  • Analyze the spot prices and find the cheapest 4 hour slot from the day (e.g. 01:00-05:00). 4 hours will be enough to heat the water to the thermostat max temperature. The 300 liters should be easily enough for one day’s hot water consumption.

  • I will not tamper the water heater’s internal logic. I will simply cut the power from it when I don’t want it to be on. This will be done so that I will connect a relay to Raspberry Pi’s GPIO and this relay will control a contactor which is connected to the power cable of the water heater. This will be the same Raspberry that will run my OpenHab.

  • Word of caution at this point: the connections from the relay output onwards involve high voltage and only authorized electricians are allowed to make those connections. Do not touch them yourself. There is a risk of serious injury or death.

Proof of concept status at the moment

I have connected a simple buzzer to the GPIO of my Raspberry Pi. I installed the GPIO binding add-on and I was able to create a Buzzer Thing and Switch Item which toggles the buzzer on and off. Hooray!

The fact that I’m able to control the buzzer via Openhab GPIO binding also means that I will be able to control a relay. From the connections point of view they are exactly the same, the GPIO 3.3. volts output toggles the relay on/off just like it toggles the buzzer on/off.

I was also able to write a proof of concept script which reads the spot prices for next day from the Entso-E Transparency Platform.

The script looks like this:

// Entso-E security token.
val String token = "insert-your-own-token-here";

// @see https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas
val String area = "10YFI-1--------U";

// @todo: investigate the timezone handling later.
val String start = now().plusDays(1).format(java.time.format.DateTimeFormatter.ofPattern("YYYYMMdd")) + "0000";
val String end = now().plusDays(1).format(java.time.format.DateTimeFormatter.ofPattern("YYYYMMdd")) + "2200";

// Make the API call to Ensto-E.
val String priceXml = sendHttpGetRequest("https://transparency.entsoe.eu/api?securityToken=" + token + "&documentType=A44&in_Domain=" + area + "&out_Domain=" + area + "&periodStart=" + start + "&periodEnd=" + end);

// Convert the response to JSON for easier handling.
val String priceJson = transform("XSLT", "xml2json.xsl", priceXml);

// Read the start time of the response.
val ZonedDateTime startUtc = ZonedDateTime.parse(transform("JSONPATH", "$['Publication_MarketDocument']['period.timeInterval']['start']", priceJson));

// Print hourly prices to log.
for (var i = 0; i < 24; i++) {
  // Read the price for this hour.
  var String price = transform("JSONPATH", "$['Publication_MarketDocument']['TimeSeries']['Period']['Point'][" + i + "]['price.amount']", priceJson);

  // Convert EUR/MWh to c/kWh and add 24% VAT.
  var Double priceWithVat = Double::parseDouble(price) * 1.24 / 10;
  
  // Print to log
  var ZonedDateTime dt = startUtc.plusHours(i);
  logInfo("entsoe", dt.toString + ": " + priceWithVat.toString + " c/kWh");
}

Output of the script:

22:26:13.284 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-10T22:00Z: 3.44968 c/kWh
22:26:13.305 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-10T23:00Z: 2.44528 c/kWh
22:26:13.329 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T00:00Z: 1.6938400000000002 c/kWh
22:26:13.350 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T01:00Z: 1.6901199999999998 c/kWh
22:26:13.372 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T02:00Z: 1.68764 c/kWh
22:26:13.393 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T03:00Z: 1.76824 c/kWh
22:26:13.414 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T04:00Z: 3.16324 c/kWh
22:26:13.438 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T05:00Z: 4.27676 c/kWh
22:26:13.460 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T06:00Z: 4.50368 c/kWh
22:26:13.480 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T07:00Z: 3.6679199999999996 c/kWh
22:26:13.501 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T08:00Z: 2.67096 c/kWh
22:26:13.523 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T09:00Z: 1.6752399999999998 c/kWh
22:26:13.547 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T10:00Z: 1.3714400000000002 c/kWh
22:26:13.568 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T11:00Z: 1.21148 c/kWh
22:26:13.589 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T12:00Z: 0.5245200000000001 c/kWh
22:26:13.611 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T13:00Z: 1.24992 c/kWh
22:26:13.632 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T14:00Z: 1.44832 c/kWh
22:26:13.656 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T15:00Z: 1.6479599999999999 c/kWh
22:26:13.677 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T16:00Z: 1.6467199999999997 c/kWh
22:26:13.698 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T17:00Z: 1.88976 c/kWh
22:26:13.722 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T18:00Z: 1.9716 c/kWh
22:26:13.745 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T19:00Z: 2.6498800000000005 c/kWh
22:26:13.770 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T20:00Z: 2.1575999999999995 c/kWh
22:26:13.792 [INFO ] [org.openhab.core.model.script.entsoe ] - 2022-06-11T21:00Z: 1.45452 c/kWh

A couple of notes about the script.

  • First of all, the proof-of-concept script only works after the day ahead prices have been published for the next day. That is, in the evening. If you want to read the prices for the current day, remove the plusDays(1).

  • Second of all, the timezone handling needs a bit of love. The script currently has an offset-by-one between Finland (EET / EEST) and Central Europe (CET / CEST).

  • The Entso-E API returns the prices in XML format. I first tried to use the XPath Transformation service to read the values from XML, but there is an issue. Despite the fact that the Entso-E XML declares a namespace, the XML elements are in the default namespace. This means that parsing the XML with XPath would turn out to be ugly as hell because I would need to use fully qualified namespaces in the XPath matching.

  • To keep the code readable, I’m first transforming the XML to JSON using witht the XSLT xml2json.xsl which is available here: Convert XML to JSON using XSLT – Bojan Bjelic

  • Credits for the inspiration of using this XML to JSON technique go to this thread: MythTV Upcoming Recordings Widget, with HTTP Bind, XML to JSON via XSLT and pagination display

Summa summarum, conclusions about the proof of concept status:

  • I’m able to read the spot prices from Entso-E API

  • I’m able to control a buzzer via GPIO

Request for guidance from the community

With all the long background from above, I’d appreciate some guidance from the OpenHab community.

Request for guidance 1: About Things, Channels, Items and Persistence

I’ve been reading the conceptual descriptions what Things, Channels and Items are, but they are quite abstract for an OpenHab newbie like me so I’m hesitating a bit.

How would you recommend me to model the hourly prices of one day? It’s essentially a key-value pair where the

  • key is the datetime of given hour, e.g. 2022-06-10T22:00Z
  • value is the spot price for that hour, e.g. 3.44968

I believe I will have one Thing called “Entso-E”

  • … but how would you recommed me to model the Channels and Items so that I would have places where I can update the 24 x hourly spot prices for each hour?
  • If I run the script twice, the expected result would be that the latter prices would overwrite the previous values for the exact same datetime keys, not creating duplicate values for the same hour.
  • If I run the script again tomorrow, the expected result is that I don’t overwrite the price of the 01:00-02:00 hour of the previous day.
  • Do you think InfluxDB or the default RRD4J is better for this persistence use case and why?
  • I obviously want to be able to plot the hourly prices for each day I have collected the data for, but the actual plotting should be straight forward based on what I have been reading about Grafana.

This is most probably very basic OpenHab stuff but I’m hesitating what is the Right Way to do this with OpenHab concepts so I would really appreciate some guidance on how to configure these different entities (channels, items) and their persistence.

Request for guidance 2. Decoupling the fetching of the hourly prices and determining the cheap hours for the water heater

The above script only fetches the spot prices and once I figure out the answers to the above things / channels / items question above with the help of the community, I can save them.

I definitely do not want to mix the “find the 4 cheap hours” to the script above. I want to de-couple it to a separate piece of code which gets executed after the spot prices have been fetched. The reason for this is that I will also have the other Thing (the heat pump) that will be using the spot prices.

The logic for determining the on/off times for the heat pump will be significantly more complex than finding the 4 cheap hours for the water heater because the heat pump controlling will need to take the weather forecast into account. But anyway, I do not want to fetch the spot prices again for the heat pump controlling, I want to use the same prices that have already been fetched once for the water heater.

So the question is: does the following sound logical way to decouple the logic in OpenHab:

  • Script 1: Fetches the spot prices and saves the values for each hour with Things / Items / Channels

  • Execute script 1 with cron once per day in the evening after the day ahead prices have been published

  • Script 2: Determines the 4 cheapest hours for the water heater for the next day. Save the results to some other Things / Items / Channels

  • Invoke script 2 either by cron slightly later than script 1 or if it’s possible, after script 1 has done it’s magic. Based on what I’ve been reading, an event based trigger would probably be more OpenHab way to invoke this script, would you agree?

  • Run a rule once per hour with cron at every full hour. If the hour changes from cheap to expensive or from expensive to cheap, toggle the relay switch on/off.

Cheers,
Markus

2 Likes

Think of “Things” as the components in the real world outside OH that are part or can be made part of your automation system. They can either be physical devices or some source of information (like your web service for energy prices). A “binding” makes them available to OH.

A “channel” is a way to access aspects of a Thing. As an example think of a multisensor that has various information available for you: temperature, open/closed contacts, humidity… For each aspect there will be a “channel”.

Items are where the data is available within OH. At runtime you will deal with Items. Each channel you want to access will be represented by an Item so you can read the data a Thing provides via the respective channel.
In addition to the items that give access to channels/Things you can freely define items you think you need to do whatever you want to do.

In essence: you will store your information in items.

The data in items can be persisted. There is a choice of persistence options for different use-cases. Very roughly:

  • MAP DB: Easiest choice for startup purposes where an item can be assigned the value it had before OH was shut down. There is only one data point for each item, no historical data.

  • RRD4J: a database that will NOT grow endlessly. Older data will be aggregated by the database itself (no admin intervention needed) so you loose precision for historical data. Can only store numerical data, so keep that in mind when you want to use it for startup data retrieval.

  • Other databases like InfluxDB, MariaDB etc.: You will have maintenance efforts for these databases, but basically no restrictions like with RRD4J or MAP-DB

Not putting the whole logic into one single script is a good approach. Especially as you want to use the fetched prices for different purposes. You may want to think about starting up the system as well and how to aquire the data needed until the next price retrieval in the evening. Persistence will help here, too.

would mean some kind of threshold definition, at least to me when reading your thoughts. That would be an additional item: prices below that threshold would be deemed “cheap”, prices above as “expensive”. That would be a way to decide if heating is allowed at the current time or not. Making that threshold item available to the users in a GUI gives them a way to intervene in certain situations. It can even be calculated from the historical price data…

Many thanks @stefan.oh ! These clarified quite a bit!

Ok, so I need to create a Thing called “Entso API”.

To create this, I need to select a Binding.

  • There is a HTTP Binding which I was first looking at but I was not able to figure out how I would be able to apply the needed logic to prepare the start and end arguments that are needed as URL arguments.
  • As I couldn’t figure out how to make the API request with the HTTP Binding, I wrote that DSL script that I shared above which uses sendHttpGetRequest to call the API.
  • Do I need to write a custom Binding which does the same thing that my DSL script, or is there already some generic dummy Binding which doesn’t do anything (and it doesn’t if the script does all the magic)?

I guess this means that for the Ensto API Thing, I would need to create 1+24 Channels and 1+24 Items.

1 Channel and Item for the date (without time) for which the hourly prices are for
24 Channels and Items for the hourly prices

Would this sound a sensible approach?

Cheers,
Markus

No, not necessarily. With your script you already found a way to aquire the data you are interested in. If it fits your needs, leave it at that for the time being if you want.

You will find out with time there is more than one way to achieve something. It is your decision if you want to learn in the beginning all the possible ways to go ahead and decide the best way forward or if you are satisfied with a working solution and maybe revisit the issue at a later point in time to improve what you got so far. Making things more robust, reduce coding with a different approach, maybe open new possibilities with a different approach…

Channels come predefined with bindings. In a way they are the glue between the items you use in OH itself (by linking an item to a channel) and the capabilities a certain Thing offers.

In your case you already have the data, so you need to store it for later use in rules. I don’t know if Rules-DSL offers arrays, in other programming languages that would be the choice for storing the same data type multiple times as needed here. Defining 24 items (one for each hour of the day) is crude, but maybe that’s what you need to do.
If Rules-DSL restricts you, be aware there are other scripting languages available for OH nowadays.

Yeah, there’s always many ways to achieve the goal, but some of them are usually more elegant than the others. I would not like to twist the platform to a tango it’s not intended.

Creating those 1+24 Items like “Spot price 00:00”, “Spot price 01:00”, “Spot price 02:00” and so on sounds indeed cumbersome. Another approach would be that I would only have one String Item where I would store the hourly prices as an serialized array in JSON format. This way I would only need one Item for the hourly prices.

Then that second script would come into play. It would run later in the evening after the spot prices for the next day have been fetched. It would read all the individual “Spot price” Items or the serialized String Item (that contains the JSON array) and determine the start time when the power for the water heater would be turned on and off. This value would then be stored into an Item “Water heater start time” and Item “Water heater stop time”.

Then, I would have a rule running on every full hour.

  • If the Water Heater GPIO Switch Item is currently on, it would check if we have reached the “Water heater stop time”. If yes, it would turn the Water heater GPIO Switch Item off.
  • If the Water Heater GPIO Switch Item is currently off, it would check if we have reached the “Water heater start time”. If yes, it would turn the Water heater GPIO Switch Item on.

The serialized JSON array sound more elegant to me, but I need to check how difficult the plotting would be.

I’d like to plot something similar like this:

  • MLP is a Finnish abbreviation for the heat pump
  • LVV is a Finnish abbreviation for the water heater

The green symbols indicate the hours of the day when they are allowed to be on. And the red symbols indicate when they are not allowed to be on, respectively.

The thin blue line is the price of the electricity on a given hour, and the bar chart tells the cumulative load that is allowed to be “on” at the same time.

Thanks for your advice again! I’ll try the multiple items vs. one JSON item approach and then start to bang my head against the plotting wall!

I have a gut feeling that more traditional database approach might turn out to be easier but I would really like to get this done with OpenHab because the framework provides so nice capabilities that I really would not like to build from scratch myself…

Cheers,
Markus

Note to self, this looks promising: OpenWeather. How to store future time series data

After sleeping one night, things start to be a bit more clear in my mind. This is what I’m probably going to do.

  1. The script will fetch the spot prices for the next day in the evening. Let this script be called “fetch-spot-prices”.

  2. The “fetch-spot-prices” script will use the InfluxDB HTTP API to write the prices with future timestamps as discussed in the thread Iinked in my previous comment. With this approach, the prices will be a true time series, which makes it easy to plot (among other benefits).

  3. Once the “fetch-spot-prices” has been executed, a second script will get invoked. Let this script be called “determine-allowed-hours-for-the-water-heater”. This script will read the spot prices from the InfluxDB and determine the hours when the water heater should be “on” and when it should be “off”. The results will be written to another time series with future timestamps.

  4. I will create a Water Heater Thing, with a Channel that connects the Thing to a Water Heater Item, which is a Switch Item. The Thing uses the GPIO Binding, which controls the relay.

  5. I will have a rule that runs every full hour.

  • This rule will check if the Water Heater is already “on”. If yes, and the “allowed-hours-for-the-water-heater” time series says that this hour is not allowed, it will turn the Water Heater Switch Item to “off”.
  • Correspondingly, if the Water Heater is “off” and the “allowed-hours-for-the-water-heater” time series says that this hour is allowed, it will turn the Water Heater Switch Item to “on”.
  1. I will make the Water Heater Switch Item to persist its state as yet another time series, which means that
  • I can plot the hourly spot prices
  • I can plot the hours that the “determine-allowed-hours-for-the-water-heater” script was saying which ones are allowed
  • I can plot the hours when the water heater was actually on / off, because the Water Heater Switch Item can be manually be toggled on/off.

Cheers,
Markus

1 Like

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

Alright, here we go. I’m sharing the code so that other community members can use this as an insipration and modify it for their needs. I’m intentionally not publishing this via github because I do not have possibility to provide support based on other people’s feature requests. But the code is here, feel free to use it for your personal needs. All code below is provided without any warranty and published with the same ISC License that Openhab uses. If you use the code, please attribute Markus Sipilä, https://www.linkedin.com/in/markussipila/

Fetching weather forecast from the Finnish Meteorology Institute’s API and saving them to the database

  • Rule “FetchWeatherForecast” has a script action, see below
  • This action invokes fmi.js which fetches the weather forecast from FMI’s API and writes the forecasted temperatures to Influx2 database as “fmi_forecast_temperature” measurement points with timestamps in the future.
  • Writing the future timestamps to the InfluxDB is done via Influx HTTP API v2
  • The script can be executed multiple times a day, possible previous database points are overwritten.

Script action:

fmi = require('kolapuuntie/fmi.js');
influx = require('kolapuuntie/influx.js');
xml = fmi.getForecast();
points = fmi.preparePoints(xml);
influx.writePoints('fmi_forecast_temperature', points);

Fetching the spot prices and saving them to the database.

  • Rule “Fetch Spot Prices” has a script action, see below.
  • The script action invokes entsoe.js, which fetches the spot prices from Entso-E API and writes the spot prices to Influx2 database as “spot_price” measurement points with timestamps in the future.
  • Writing the future timestamps to the InfluxDB is done via Influx HTTP API v2
  • Entso-E publishes the spot prices for next day in the afternoon. date-helper.js module has logic that if the script is executed at 15:00 or later, it will fetch tomorrow’s prices. If the rule is executed before that, it will fetch current day’s prices.
  • The script can be executed multiple times, possible previous database points are overwritten.

Script action:

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

// Get date range in the correct format for Entso API
start = dh.getEntsoStart();
end = dh.getEntsoEnd();

// Read spot prices and write them to the database.
points = entsoe.getSpotPrices(start, end);
influx.writePoints('spot_price', points);

Determining the on/off hours for the waterheater

  • The following script action invokes waterheater.js which will read the previously saved “spot_price” measurement points from the Influx database.
  • Reading from the InfluxDB is done via Influx HTTP API v2
  • date-helper.js determines the start and stop midnights based on when the script is executed. Same today vs. tomorrow logic applies that I used with Entso API (cutoff time is at 15.00).
  • It will find the cheapest 4 hours when the waterheater will be allowed to be on.
  • The “control points” are stored in InfluxDB as “waterheater_control” measurement points. Value 1 means that the waterheater should be on and 0 means that it should be off. There is a point for every hour, either 1 or 0.

Script action:

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

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

wh.determineHours(start, stop, 4);

Determining the on/off hours for the Nibe ground source heat pump

  • This script action invokes nibe.js which will first read the “fmi_forecast_temperature” measurement points from the Influx database.
  • Based on the average temperature for the day, it 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, we will 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.

Script action, the day is split to 3 slices and each slice must have 10% of the heating hours:

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

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

t = nibe.getForecastTemp(start, stop);
n = nibe.calculateNumberOfHours(t);
nibe.determineHours(start, stop, n, 3, 0.1);

Hourly script to toggle the waterheater on/off when needed.

  • This script action will read the currently ongoing hour’s “waterheater_control” point from the InfluxDB
  • If the waterheater GPIO switch item is currently ON and the “waterheater_control” point has value 0 for the hour that just started, the waterheater switch will be set to OFF. And vice versa, if the waterheater needs to be set to ON.
  • Similar script action exists for the Nibe heat pump
dh = require('kolapuuntie/date-helper.js');
wh = require('kolapuuntie/waterheater.js');
influx = require('kolapuuntie/influx.js');

start = dh.getCurrentHour();
control = influx.getCurrentControl('waterheater_control', start);

waterheater = items.getItem("waterheater_switch");

// Check if the waterheater is off and should be turned on.
if (waterheater.state == 'OFF' && control == 1) {
  waterheater.sendCommand('ON');
  console.log('Waterheater ON.');
}
// Check if the waterheater is on and should be turned off.
else if (waterheater.state == 'ON' && control == 0) {
  waterheater.sendCommand('OFF');
  console.log('Waterheater OFF.');
}
else {
  console.log('Waterheater: No state change needed.');
}

Helper modules used in the script actions above
As mentioned in the beginning, the code is provided without any warranty and published with the same ISC License that Openhab uses. If you use the code, please attribute Markus Sipilä, https://www.linkedin.com/in/markussipila/

These files are placed at /etc/openhab/automation/js/node_modules/kolapuuntie. Change the name of yor module as you see best. Remove the .txt file extension which was required to upload the files here.
fmi.js.txt (2.0 KB)
date-helper.js.txt (2.1 KB)
entsoe.js.txt (2.8 KB)
influx.js.txt (6.4 KB)
nibe.js.txt (6.0 KB)
waterheater.js.txt (2.5 KB)

Technical software components

  • I’m running this on Raspberry Pi 4, with Openhabian 64-bit OS
  • I’m running InfluxDB2 (which requires 64-bit OS)
  • The custom javascript files are located at /etc/openhab/automation/js/node_modules/kolapuuntie. Rename your module folder as you see best and modify the code above accordingly.
  • The JSScripting addon is installed and all rules are written as ECMAScript 262 Edition 11
  • The InfluxDB Persistence addon is installed, for normal persistence reasons. As mentioned above, writing the future-timestamped spot_price and waterheater_control measurement points is done via the InfluxDB HTTP API.
  • XSLT Transformation addon must be installed.
  • JSONPath Transformation addon must be installed.

A couple of gotchas

  • Entsoe-E API returns the spot prices in XML format. There is no JSON available.
  • I’m first transforming the XML to JSON using a xml2json.xsl which is available here: Convert XML to JSON using XSLT – Bojan Bjelic
  • This file is saved to /etc/openhab/transform/xml2json.xsl
  • Entso-E API seems to take input parameters in CET/CEST but responds in UTC.
  • My time zone is EET/EEST so there might be some timezone offsets that you need to modify if you are on a different timezone. All data is written to the database in UTC.

Some screenshots
Pictures tell more than a thousand words… The below screenshot from the InfluxDB data explorer shows two different graphs.

  • The blue line represents the hourly spot prices for 2022-06-29. As you can see there is a huge difference in the hourly prices, the prices vary between 3.3 c / kWh to 48.5 c / kWh so it really does matter what time I heat the water…
  • The violet line represents the hourly control values for the waterheater. On 2022-06-29 the cheapest 4 hours are from 00:00-04:00 in the night.

Hardware and Things

  • I have one Thing called the Waterheater. It has a GPIO Binding and a Switch Item.
  • I have another Thing called Nibe. It also has a GPIO Binding and a Switch Item.
  • When I was developing this, I had a 3.3V buzzer connected to a GPIO output pin
  • I will replace the buzzer with relays, to be more specific this: 8-kanavainen relekortti, Raspberry pi DIN-kiskoon / RASPI-RELE8-DIN - Triopak Oy
  • One relay will control a Contactor, which will cut / allow power supply to the water heater. This is 400 V in 3 phases and the connection can only be made by an electrician. I repeat my warning from the beginning of this thread: This connection must only be done by an authorized electrician. High voltage can kill you.
  • Second relay will control the Nibe heat pump’s AUX input, which can be configured to allow / disallow the compressor to run.

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.