Spot Price Optimizer: Advanced algorhitms to optimize heating, charging of electric vehicles, water boilers and more

I just released version 3.0.0 of the openHAB spot Price Optimizer, which has evolved from this original discussion.

I’m starting this new thread for the sake of simplicity because the concepts have gone through such an evolution that it’s hard to go through the 360+ comments of the original thread.

Spot price electricity contracts and demand response

Spot priced electricity contract means that the price of electricity is different for every hour of the day. The day-ahead prices for most European countries are published at around 13.15 CET/CEST on the Entso-E Transparency Platform.

This solution helps to automatically schedule and optimize the consumption of electricity to the cheapest hours. The concept is referred as demand response in electrical grid. If your electricity contract is based on spot prices, you can save significant amount of money using this solution.

The solution can be applied for a variety of devices. The openHAB community members are using this to optimize the heating of their houses, heating domestic hot water in a water boiler, charging an electric vehicle when it’s cheap and heating the water of swimming pools.

The key concept is to calculate control points for the next day, which define when the device is expected to be ON or OFF (or have its other kind of state changed e.g. lower the target temperature when prices are high and increasing the target temperature when prices are low). The picture below illustrate two use cases: heating the domestic hot water and heating of a house.

The blue area represents the hourly spot prices of electricity. The red bars are the control points for heating the domestic hot water in a boiler during the three cheapest hours of the day. The yellow bars are the control points that control when the heating of a house is ON. On this example day, 14 hours of heating is distributed so that the morning and evening price peaks are avoided.

Complete instructions and detailed step-by-step tutorial
Complete installation and detailed step-by-step instructions are maintained in the project Github wiki, which can be found from the link below.

Roadmap for openhab-spot-price-optimizer version 4.0

Release notes
I will use this thread for publishing the release notes of openHAB spot price optimizer. The release notes for the brand new 3.0.0 version can be found below.

6 Likes

Release notes for version 3.0.0

Update instructions: Installation instructions · masipila/openhab-spot-price-optimizer Wiki · GitHub

List of changes:

Change code to use 2 space indents instead of tab

  • No impact to existing Rules, change is backwards compatible to version 2.0.x.

entsoe.js:

  • Increased the API call timeout from 15 to 45 seconds
  • Normalized TimeSeries and Period elements to be always as arrays due to EntsoE breaking their API in an uncontrolled fashion in October 2024.
  • No impact to existing Rules, change is backwards compatible to version 2.0.x.

heating-calculator.js:

  • Added validations for heat curve and forecast
  • Bugfix: Fix the logic when heating need is calculated for shorter period than 24H and the result is max or min of the heat curve
  • No impact to existing Rules, change is backwards compatible to version 2.0.x.

generic-optimizer.js:

  • Dropped support for other time series resolutions than 15 minutes. entsoe.js has been writing the prices with 15 minute resolution already in previous versions so this change is backwards compatible unless you are simulating optimizations with your historical price data which has 60 minute price resolution.
  • All optimization methods now support optional startConstraint and endConstraint arguments. This change is backwards compatible since these arguments are optional.
  • allowIndividualHours, blockIndividualHours now accept the requested time to be a float, so you can now request for example 1.25 hours. Previously this was rounded up to next full hour.
  • New optimization methods are documented in wiki
  • Change is backwards compatible to version 2.0.x with the remark of optimization simulations with historical price data that has resolution of 60 minutes.

tariff-calculator.js:

  • Bugfix: fix time comparison to properly use time.isBefore(). Bug reporting credits to @Jak
  • Bugfix: fix calculatePriceSeasonalDistribution arguments
  • Dropped support for other time series resolutions than 15 minutes, tariff and total prices are now always saved with 15 minute resolution.
  • Change is backwards compatible to version 2.0.x.

Introduced completely new optimization algorithm called heating-period-optimizer, see documentation and usage example in wiki.

Added test automation coverage (134 automated test cases at the moment) to avoid regression in the future releases.

1 Like

Unfortunately I had to make a new release soon after releasing the version 3.0.0. Sorry for the extra inconvenience for those who had just updated to 3.0.0. :frowning:

Update instructions are available at https://github.com/masipila/openhab-spot-price-optimizer/wiki/Installation-instructions

Changes in version 3.0.1

EntsoE changed their API on October 1 so that they use the A03 curve type instead of A01 which they did previously. In practice this change means that when two consecutive hours have exactly the same price, they now only have one element in their response - previously they had both hours explicitly present. This is the reason why some hours seemed to be magically missing on some days.

Their announcement about the API change is quite well hidden at News, it’s mentioned in the announcement on Sep 30 related to data migrations.

Changes in version 3.0.2
I made a mistake with test automation with release 3.0.1, which is fixed in 3.0.2

I guess this means that your spot price optimizer will become agnostic to the service delivering the spot prices, since you will be integrating through items? It will then also work with Energi Data Service, Tibber and aWATTar since they all support time series already?

Yes, that’s exactly the idea. This optimizer can already now be used with any price and weather forecast provider, given that the data is found as a proper timeseries.

I would really want to deprecate and remove the EntsoE price fetching and the FMI weather forecast fetching from this package since they are completely separate topics. This optimizer simply needs the price and weather forecast timeseries but it doesn’t mean that those architecturally belong to this package.

I am aware that there is at least one weather forecast binding available which saves the data as timeseries. But the FMI weather forecast data this solution fetches is curated by a real human meteorologist i.e. it’s much more accurate than alternative data sources which are only providing data from one weather model. So if somebody is interested to update FMI Weather binding so that it

a) uses the fmi::forecast::edited::weather::scandinavia::point::simple data provider by FMI meteorologist instead of using the Harmonie weather model

b) saves the data as a proper timeseries

… then we would be able to use that binding instead of maintaining it here in parallel.

Markus

I don’t know what this means, and probably can’t help with that, but:

I have given this a shot, although I wish I would have read A more carefully, because I realize now that B will probably be of no use to you without A. Nevertheless - here we go:

See [fmiweather] Add time series support for forecasts by jlaur · Pull Request #17543 · openhab/openhab-addons · GitHub

Markus, you are doing a great job taking on this very complex optimization task. I did the update and found that the new scripts apparently make some of the old rules obsolete. Please confirm II have got this right:

  1. CalculateHeatingHours is replaced by the new rule script that is run by FetchSpotPrices
  2. HeatPumpCompressorOptimizer is replaced by the new script that is run by FetchSpotPrices
  3. UpdateAverageSpotPrice is depreceated as it is dependent on datehelper. I am not sure if this rule is my own creation, though.

I disabled the above rules and so far everything seems to run OK. This winter is sure going to be interesting :wink:

@laursen it’s just one arguments in the API request. This is what I use:

const url = 'http://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple&place=' + place + '&parameters=Temperature,PrecipitationAmount,WindSpeedMS,TotalCloudCover';

I believe the FMI Binding is referring to harmonie in the storedqueryid

  1. Correct, we previously had a separate Rule for claculating the heating hours for the entire day. The HeatingPeriodOptimizer now calculates the need in the fly for each of the heating periods. And the HeatingPeriodOptimizer scripting is now indeed as an additional Action in your FetchSpotPrices Rule.

  2. Correct again. The new action that contains the scripting for HeatingPeriodOptimizer is actually the optimizer for your compressor, but it’s now directly as an additional action in your FetchSpotPrices Rule. It’s a matter of taste how you want to do this because Rule Action can trigger the execution of other Rules like you had before. So you could have as well replace the old scripting in HeatPumpCompressorOptimizer Rule and call that from FetchSpotPrices like you had previously.

  3. This is something your own stuff.
    Markus

You could try this then: org.openhab.binding.fmiweather-edited-4.2.3-SNAPSHOT.jar (remove “-edited” from the filename)

I don’t know if you prefer 4.2 or 4.3 JARs? The place parameter is missing, so I don’t know if it’s working. What should it contain?

This test:

was adapted to new result:

        assertThat(request.toUrl(),
                is("""
                        https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple\
                        &starttime=2019-03-10T11:01:04Z&endtime=2019-03-10T11:01:05Z&timestep=61&latlon=9,8\
                        &parameters=Temperature,Humidity,WindDirection,WindSpeedMS,WindGust,Pressure,Precipitation1h,TotalCloudCover,WeatherSymbol3\
                        """));

Sorry, it doesn’t work. Something more is needed: “Unexpected API response: No locations in response – no data? Aborting”

Do you have an issue or thread for the FMI Weather work that you’re doing, I’m happy to participate with test efforts there?

Anyway, the place parameter accepts a name of the place that is recognized by the FMI API. Here’s an example of the correctly formatted URL for an API request for requesting the forecast to Helsinki.

http://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple&place=helsinki&parameters=Temperature,PrecipitationAmount,WindSpeedMS,TotalCloudCover

Markus

You’re right, let’s branch out to a new thread. I implemented time series support as a reaction to this post: Spot Price Optimizer: Advanced algorhitms to optimize heating, charging of electric vehicles, water boilers and more - #5 by masipila.

I don’t personally use the binding, so I gave myself a one hour timebox for this effort. :slight_smile: It can already be tested: [fmiweather] Add time series support for forecasts by jlaur · Pull Request #17543 · openhab/openhab-addons · GitHub.

1 Like

I opened up a feature request for the FMI Weather binding, see [fmiweather] Use fmi::forecast::edited instead of fmi::forecast::harmonie · Issue #17548 · openhab/openhab-addons · GitHub

1 Like

Hello,

List of changes says:

  • generic-optimizer.js:
    allowIndividualHours, blockIndividualHours now accept the requested time to be a float, so you can now request for example 1.25 hours. Previously this was rounded up to next full hour.

If I change my rule

// Read how many hours are needed from the HeatingHours item.
heatingItem = items.getItem(“HeatingHours”);
//heatingHours = Math.round(heatingItem.state); // Does it have to be an integer ?
heatingHours = heatingItem.state; ← this causes an error

Error:
2024-10-12 17:16:41.430 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - generic-optimizer.js: price window 2024-10-12T21:00Z - 2024-10-13T21:00Z (PT24H)
2024-10-12 17:16:41.433 [ERROR] [vascript.HeatPumpCompressorOptimizer] - Failed to execute script: TypeError: hours.toFixed is not a function
at .allowIndividualHours(/etc/openhab/automation/js/node_modules/openhab-spot-price-optimizer/generic-optimizer.js:188)
at .:program(:94)
at org.graalvm.polyglot.Context.eval(Context.java:399)
at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426)
… 22 more
2024-10-12 17:16:41.433 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID ‘HeatPumpCompressorOptimizer’ failed: org.graalvm.polyglot.PolyglotException: TypeError: hours.toFixed is not a function

The same happens also for allowPeriod(hours). Maybe I misunderstood that change log. I use only the generic-optimizer.

BR; Jakke

You need to pass a float as an argument. item.state returns a string.

You can do it like this. I have an Item which contains a value 2.25 at the moment. And both examples below work.

// Load modules.
var { GenericOptimizer } = require('openhab-spot-price-optimizer/generic-optimizer.js');
var { Influx } = require('openhab-spot-price-optimizer/influx.js');

// Create services.
var influx    = new Influx();
var optimizer = new GenericOptimizer();

// Define the optimization window.
var start = time.toZDT('00:00').plusDays(1);
var end = start.plusDays(1);

// Read spot prices for the optimization window and pass them to the optimizer.
var prices = influx.getPoints('SpotPrice', start, end);
optimizer.setPrices(prices);

// Read desired amount of hours from the item.
var item = items.getItem('BoilerHours');
var hours = parseFloat(item.state);

// Find the cheapest consecutive period and block the rest.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();

// Get the control points and write them to the database.
var points = optimizer.getControlPoints();
influx.writePoints("BoilerControl", points);

And if you want to it in bunches of hours, but the hours don’t need to be consecutive, then use allowIndividulaHours() instead of allowPeriod(). The remainder (0.25h in this example of 2.25h) will be allocated to the first hour, so that one will be 1.25h and then there will be a second, 1h allowed period which may or may not be consecutive to the first 1.25 period.

// Find the cheapet time and block the rest.
optimizer.allowIndividualHours(hours);
optimizer.blockAllRemaining();

I’ll take a note to self that I will harden the GenericOptimizer in the next version so that it will try to cast strings to floats if the input argument is a string (and use it directly as float if it’s float already). I don’t want to create yet another release within a couple of days because you can just wrap the item.state with parseFloat().

Cheers,
Markus

Hello

This is fine, and thanks for the explanation! My intention would be to use allowIndividulaHours() for heating and allowPeriod() for boiler.

BR, Jakke

1 Like

Complete usage examples for the different optimization methods provided by the GenericOptimizer have now been added to the project wiki:

Happy optimizations to everyone! Let’s do our share to fight the climate crisis by optimizing our electricity consumption to the times when most of the production is done with renewables (and prices are at lowest).

4 Likes

I love your work Markus! I’ve experimented with this for a few years, but never got a good result. I’m excited to test this!

I have a question regarding the FMI Weather API. In the rule you’re supposed to add a place. I couldn’t find any information about which places are supported. When I check which weather stations they have it’s (obviously) just in Finland. Are other places also supported (I live in a suburb to Stockholm)?

I randomly tested ‘stockholm’, ‘solna’ and ‘kista’ and they all seem to work, see

http://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple&place=stockholm&parameters=Temperature,PrecipitationAmount,WindSpeedMS,TotalCloudCover

http://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple&place=solna&parameters=Temperature,PrecipitationAmount,WindSpeedMS,TotalCloudCover

http://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::edited::weather::scandinavia::point::simple&place=kista&parameters=Temperature,PrecipitationAmount,WindSpeedMS,TotalCloudCover

You can try your own suburb first by changing the place argument with a normal web browser like in the example URLs above and if you see a proper XML response, then it will also work with openhab-spot-price-optimizer.

Cheers,
Markus