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

I don’t think that is the reason. If some of your Rules work but other don’t, then there is most probably something wrong with the inline script actions of those Rules.

The influx connection parameters must be defined in two places:

  • openHAB persistence settings (openHAB uses these connection parameters to access the database)
  • config.js of this module. The script actions of your rules bypass openHAB persistence layer and interact directly with the HTTP API provided by your influxdb.

Once you have double checked this, re-save the Rule FetchSpotPrices and run it again. Verify that it is working by inspecting the logs.

Then re-save the Rule that you are trying to do and inspect the logs. Then, please provide the complete script action of your Rule here, together with the log outputs.

Cheers,
Markus

Thank You Markus for your sharp advise! I hadn’t updated the connection parametres in InfluxDB Persistence at Add-on Settings, when I recently altered the IP of my db-server and also at same time took a new Influx-token for the config.js file, when setting up the new configuration. Now the Boiler Optimizer works just fine and writes in the database, and the result can be seen on Grafana dashboard panel. No errors at log. Next I will setup the HeatPump implementation once more, presumable it will work fine as well! Have a nice evening Markus and Big Thanks! No need to bother you more on this issue, I hope :slight_smile:

1 Like

Version 4.0.0 has now been released. This is a major upgrade which means you need to do some changes to your existing Rules and Items.

  • Starting from this version, openHAB Spot Price Optimizer no longer provide the capabilities to fetch spot prices from Entso-E API or to fetch weather forecasts from Finnish Meteorology Institute’s API.
  • openHAB version 4.2.0 introduced capability to persist timeseries data with future timestamps and the Entso-E and FMI Bindings now do exactly this. Since there are Bindings dedicated to this task, there is no point to maintain a “competing” solution in this module. Furthermore, openHAB Spot Price Optimizer is now agnostic to the price data and weather forecast data provider, the only requirement for those is that the Binding stores the data as a proper timeseries with openHAB forecast persistence capability.

Deprecation notice for version 3.x
Version 3.x is now deprecated and will reach end-of-life on 31 July 2025. All new features will go only to version 4.x. Bug fixes and community support will be provided for version 3.x until the end of July 2025.

Upgrade instructions

  • First things first:
    • Version 4.0.x of the openHAB Spot Price Optimizer relies on Entso-E Binding (or any other price provider binding) that can store the data as proper forecast timeseries. The Entso-E Binding will be included in the openHAB 4.3.0 which is scheduled to be released on 15 December 2024.
    • Version 4.0.x of the openHAB Spot Price Optimizer relies on weather forecast binding that can persist the weather data as proper forecast timeseries. The FMI Weather Binding support this from openHAB version 4.3.0 onwards.
  • If you want, you can use openHAB 4.3.0.M4 pre-release or even install the Entso-E and FMI Wether bindings manually, but I recommend waiting for the official openHAB 4.3.0 release and proceed with openHAB Spot Price Optimizer upgrade after that.
  • The upgrade process requires several steps, including update of your own Rule scripts because the way how we interact with your influx database has changed. The upgrade steps have been tested and fully documented with the normal gold-level standard of this module. Detailed upgrade instructions are available in the project wiki.

New features
Version 4.0.0 introduces three new features for the HeatingPeriodOptimizer, which are all optional.

  1. Heating need adjustment allows you to temporarily decrease or increase the heating. This is useful for example when you are away from home for a vacation. Previously you would have needed to adjust the heat curve but now this can easily be done with an Item.
  2. Heat curve can now have multiple points. Previously the heat curve was made of exactly two points, which defined a linear curve for calculating the heating need. Now the curve can be configured in multiple pieces if the heating need of your house is not fully linear.
  3. Heating period overlap is now configurable, it was previously hard coded to 1 hour.

Removed files
The following files were supposed to be remobed in version 4.0.0 but I forgot to do that so they are removed only in 4.0.1 and 4.0.2

  • config.js is no longer needed because the Entso-E security token needs to be configured in the Entso-E binding. InfluxDB connection parameters are already configured in openHAB persistence settings and that’s enough now that our module utilizes that service and no longer bypass it
  • entsoe.js has been removed, as discussed above
  • fmi.js has been removed, as discussed above
  • influx.js has been removed, as discussed above
  • peak-period-optimizer.js is removed. PeakPeriodOptimizer was the legacy heating algorithm which was deprecated already earlier when version 3.0.0 was released. The HeatingPeriodOptimizer is the way to go for optimizing the heating of houses since it provides significantly better and more reliable results.

Support the development
In addition to being home automation enthusiast, I’m also semi-professional athlete targeting the 2026 Winter Olympics in mixed doubles curling. If you like the Spot Price Optimizer and want to support our journey towards Milano-Cortina, you can do that by donating a sum of your choice to our curling club’s bank account. Fundraising in Finland requires a permit, our permit number is RA/2024/1837. The fund raising permits are always valid for only a limited time so if you like this module, please consider donating and supporting a good cause.

  • Bank account holder: Kisakallio Curling Club ry (VAT ID FI30644384)
  • IBAN: FI94 5280 0020 0319 02
  • BIC: OKOYFIHH
  • Reference number: 20006
  • Message (if you can’t use the reference number): openhab optimizer

Cheers,
Markus

Edit: I released versions 4.0.1 and 4.0.2 to remove the entsoe.js, fmi.js, config.js, influx.js and peak-period-optimizer.js which I forgot to remove earlier. Also added a notice about this to the release notes above.

1 Like

I just realized that this is already now possible and can be done without changes to the openHAB Spot Price Optimizer, all the needed logic can be written to Rule scripts.

If somebody is interested, you can achieve this like this:

Detecting if additional heating is needed

  • Have an indoor temperature sensor and create an Item for example TemperatureBedroom
  • Create a Rule, for example ExtraHeatingOptimization which is run when the state of TemperatureBedroom changes AND the temperature is below your comfort zone, for example 19.5 °C

If additional heating is needed, you can add more heating need for the period between now and for example the next 6 hours. This Rule action could look something like this:

// Load modules and create service factory.
var { HeatingPeriodOptimizer } = require('openhab-spot-price-optimizer/heating-period-optimizer.js');
var { ServiceFactory }         = require('openhab-spot-price-optimizer/service-factory.js');
var serviceFactory = new ServiceFactory();

var parameters = {};

// Required item name parameters
parameters.priceItem    = "SpotPrice";                        
parameters.forecastItem = "FMI_Weather_Forecast_Temperature";
parameters.controlItem  = "HeatPumpCompressorControl";

// Required optimization parameters, heat curve can have 2 or more points.
parameters.numberOfPeriods = 1;
parameters.heatCurve = [
  {temperature : -25, hours: 24},
  {temperature : 2, hours: 7}, 
  {temperature : 13, hours: 2}
];

// Optional parameters, remove if you don't want to use these advanced features on this Rule.
parameters.periodOverlap   = 0;
parameters.dropThreshold   = 2;
parameters.shortThreshold  = 0.5;
parameters.gapThreshold    = 1;
parameters.shiftPriceLimit = 2;

// Read previous heating need adjustment, divide by 4 and add 1 hour.
parameters.heatingNeedAdjustment = items.getItem('HeatingNeedAdjustment').numericState / 4 + 1;

// Define the optimization window here
var start = time.toZDT()
var end   = start.plusHours(6);

// Read prices from the database, optimize and save the control points.
var heatingPeriodOptimizer = new HeatingPeriodOptimizer(start, end, parameters, serviceFactory);
heatingPeriodOptimizer.optimize();
var timeseries = heatingPeriodOptimizer.getControlPoints();
var controlItem = items.getItem(parameters.controlItem);
controlItem.persistence.persist(timeseries);

It is worth mentioning that in the example above, the numberOfPeriods parameter is set to 1 (I normally have 4 (i.e. 4 x 6h periods) and optimization period is from now to the next 6 hours. The heatingNeedAdjustment is a value for the normal use case i.e. optimization of the entire day, so the item state is divided by 4 so that we get this extra period’s share of the additional heating need. And then we add one more hour to get the indoor temperature up to the comfort zone.

The result of this hypothetical example situation would look like this:

Heating control points before are illustrated in the figure below.

  • I use periodOverlap to be 1 hour.
  • All of the heating need of the 05:00 - 13:00 period takes place in the beginning of this period
  • The non-flexible heating need for the period 11:00-19:00 is 1h 30 minutes. 30 minutes of this take place at 15:00 and the rest happen at the end of this period starting at 19:30 (the gap shifting feature postponed this heating slightly so that’s why it starts this late)

The ExtraHeatingOptimization Rule was optimizing just the period from 15:00 - 21:00. With the example parameters illustrated above, the result looks like this:

  • There is 30 minutes more heating starting at 15:00
  • And then the heating goes on at 18:00 instead of 19:30

Summa summarum
I would primarily recommend to find optimization parameters for the day-ahead optimization which are not too agressive.

  • The first parameter to adjust is the periodOverlap and have it at zero. If you’re having 4 x 6h periods, this guarantees that every 6h period will always have at least some heating (the non-flexible heating need)
  • The second parameter to adjust is the flexDefault. If you need to guarantee more heating need for each 6h period, use a low flexDefault. For example the value 0 would mean that none of that period’s heating need is moved to other (cheaper) times of the day.

If your house is well insulated and you can normally have more agressive optimization like I can, you could benefit of this kind of intraday optimization as illustrated with the example above.

Happy optimizations to everyone!

Cheers,
Markus

1 Like

We just qualified for the next World Championships (April 2025) and earned a spot to the Olympic Qualification Event which will be held in December 2025 just before the Olympics.

Many, many thanks to all of you who have already donated and are supporting our journey! Your contributions do matter!

Markus

2 Likes

Hi everybody,

I updated my production environment today to openHAB 4.3.0 and to openHAB Spot Price Optimizer 4.0.2.

If you’re planning the update, I would recommend to wait until I have updated the instructions in the project wiki. I found a couple of things that I did not spot in my development environment so I will write those key gotcha’s to the wiki so that the update process would be more hassle free for the other community members.

I’ll write an update here once the wiki instructions have been updated + share my observations.

Cheers,
Markus

Okay, the upgrade instructions have now been updated to the project wiki.

Important point 1:

The key thing to remember is now better highlighted under the subheading “Considerations for existing price data”.

  • The existing price data is stored with 15 minute resolution whereas the Entso-E provides it with 60 minute resolution.
  • The existing price data MUST be overwritten with 60 minute resolution data so that when you run your optimization the next time, it has consistent 60 minute resolution data for the whole optimization window.

Finland is one hour ahead of CET, which means that the pricing window is from 01:00 to 01:00 instead of 00:00 to 00:00. Since I run my optimizations from midnight to midnight, the first hour (00:00 - 01:00) had data with 15 minute resolution and the rest of the day had 60 minute resolution. This caused a little bit of scratching of the head to me…

If you are using the optional TariffCalculator to calculate the grid distribution fee and total price, the same thing applies here: You must overwrite this data with 60 minute resolution if you are optimizing against Total Price instead of Spot Price. Instructions for this are provided on the same section linked above.

Important point 2:
A hugely beneficial capability to exclude items or item groups from persistence strategies was added to openHAB 4.3.0 just before the feature freeze of the 4.3.0. The openHAB Spot Price Optimizer wiki pages now recommend to use this new great improvement, details can be found on the project wiki pages and are also clearly highlighted in the upgrading instructions linked above.

If you appreciate these documentation efforts, please consider donating for our Olympic journey. The details of this fundraising can be found in the previous comments of this thread.

Cheers,
Markus

1 Like

I got a feature request via a private message related to different kind of grid tariffs compared to what is currently supported by the (very simple) tariff calculator.

At least in Norway there is this kind of tariff scheme which encourages the consumers to spread their load so that there would be less sharp peaks:

Optimizing against this objective introduces one new optimization target, but is conceptually fully doable. I’ll use our house as an example here. I know that

  • base consumption is around 650 W during winter
  • electric vehicle charger is 11 kW
  • sauna is 11 kW
  • boiler is 3 kW
  • ground source heat pump is 2 kW
  • Washing machine, dish washer, oven etc obviously come on top of this. I’ll discuss them later below.

This means that I can not avoid peaks which would be less than 11 650 W. So with the Norwegian grid pricing scheme our house would fall into the 10-15 kW category. But I could avoid the next categories and save up to 3960 NOK per year which is is about 580 EUR by keeping the peaks under 15 kW.

Optimizing the schedules for all of this is conceptually doable but I would like to brainstorm the logic with the community. Here are my initial thoughts.

First of all, we need to define the max concurrent load for the optimizations. Let it be 14.5 kW in this example.

We would have one timeseries Item in addition to the control points items that we currently have. Let this be called ConcurrentLoad.

The optimizer would first allocate the base load to this timeseries. Then, it would start from the biggest load, I.e. the EV charger in our case. I can use the time constraints (earliest start, latest end) which are already now present in GenericOptimizer. Let’s say that I have defined that I need 3 hours to charge the car. GenericOptimizer would find the cheapest consecutive period, let’s say it would be from 02:00 until 05:00. The ConcurrentLoad time series would now contain value 11650 for this time period and 650 for all others.

I obviously don’t control our sauna via optimizations so the next thing to optimize would be the boiler. Let’s say I need 2 hours for that. It consumes 3 kW so the optimizer would find the cheapest period which respects the max threshold 14.5 kW. Since 02:00-05:00 already has 11650W, it would not schedule the boiler to this period but to the first possible cheapest period. Let this be 00:00-02:00.

So the timeseries would now have 3650W for 00:00-02:00, 11650W for 02:00-05:00 and 650 for all other times.

It would then proceed to the optimization of the heating. It takes 2000W so that can be scheduled anywhere because the 14.5 kW threshold is not exceeded even if the heating occurs at the same time with the EV charging.

When it comes to dish washer and washing machine, the only thing to keep in mind is that they would not be ON at the same time as EV charging + ground source heat pump. I schedule them in the evening (using their built in timers) so that they run during the night and so that the washing machine stops just before I wake up. But I can easily use the time constraints of the GenericOptimizer to force the EV charging to take place for example between 22:00 and 04:00 so that 04:00-07:00 is always available for washing machine and dishwasher.

This would mean that the only remaining thing is to keep an eye for the other loads when heating the sauna (typically in the evening). In practice: don’t have the oven or dish washer ON at the same time with sauna.

Thoughts anyone?

Cheers,
Markus

Thanks Markus for your valued insight. I was the one asking if you had looked at this before. I just recently got the spot price optimizer working, and wanted to look into how to calculate and possibly regulate the distribution pricing here in Norway.

After researching this some more, it actually looks like the distribution price, at least for me, will be a combination of the night tariff and this capacity tariff. My local network operator has an 06:00 → 22:00 → 06:00 night tariff, while the state has defined this capacity tariff for the whole country.

For price calculation it would be needed to chain two tariffs, to be able to get the price, and this will only be an estimate, as the real usage will actually determain the real price.

For actual heating optimising, the use of ConcurrentLoad sounds like a good starting point. This will provide a way to to handle to really big loads during the night.

The real problem with this capacity tariff is “unschedlued” power usage, some of it is predictable, like 07:00 → 08:00 and 16:00 → 18:00, and some is not predictable at all. To handle the predictable usage, it could be an option to just define a heat curve period during these hours to avoid any scheduled usage, so to free up space for unschedlued loads, not ideal, but might work. Having one Heating Period Optimizer for regular weekdays and one for weekends could handle that quiet well.

The not predictable usage could be handled by making some of the applicanes like heating and boiler just turn off if max current was reach, but having some kind of max expiry time to avoid long periods of heating.

Espen

I strongly suggest you to break this into pieces.

The first and easiest topic is the night tariff.

The night tariff you described is identical to what we have in Finland, the only difference is that the night tariff is from 22:00 to 07:00 whereas for you it is until 06:00. I can probably make these times configurable in future versions but now it’s hard coded to 07:00.

So what you want to do is to create two new Items, DistributionPrice and TotalPrice, and then use the TariffCalculator to populate these two timeseries as instructed here: 4.x ‐ Calculate distribution and total price · masipila/openhab-spot-price-optimizer Wiki · GitHub

Then, you want to run your optimization Rules against TotalPrice instead of SpotPrice.

Then, the second topic is to spread the load so that you don’t have all big loads ON at the same time.

For this exercises, you need to first analyze and understand the loads of your house like I did in the example above. The result of this exercises is the conclusion of the category that you want to aim. If you fir example have a 3 kW boiler, it’s impossible to stay in the cheapest 0-2 kW price category.

Once you have listed all your big loads / devices and understand their usage patterns, then you can start thinking what would be possible and what not. But don’t over engineer this. The heating of the house is definitely something that you do not want to restrict, there are already so many competing objectives that it’s complex enough already. So my advise is to just understand how much power your heating system consumes. If that would be for example 2kW like our ground source heat pump and your base load is for example 650W and your oven peaks at 2 kW, then you will just have to accept that these things will be ON at the same time.

Once you understand the loads of your house, you can think about their scheduling. Do you need to heat domestic hot water multiple times per day or will there be enough hot water for the whole day. And so on.

Markus

Okay, the capacity tariff will most likely get more and more common also in Finland during 2025.

Here’s an article in today’s Helsingin Sanomat (in Finnish) about this:

Energia | Sähkön siirtolaskuun saattaa ilmestyä ensi vuonna uusi yllätyserä: https://www.hs.fi/talous/art-2000010917762.html

I’ll probably implemented this feature to 4.x version of the openHAB Spot Price Optmizer at some point. Let’s see if I will have time before February. If not, then it will be only in summer because from Feb 6 onwards we will have 6 tournaments in 9 weeks so I will be home just enough to wash my clothes before the next competition.

Markus

1 Like

Having some issues with getting the tariff calculator to work. It is failing with:

2024-12-27 16:18:15.270 [INFO ] [nhab.automation.script.file.power.js] - tariff-calculator.js: Calculating distribution and total prices...
2024-12-27 16:18:15.275 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job 'org.openhab.automation.script.file.power.js' failed and stopped
org.graalvm.polyglot.PolyglotException: ReferenceError: "start" is not defined
	at <js>.calculate(/etc/openhab/automation/js/node_modules/openhab-spot-price-optimizer/tariff-calculator.js:50) ~[?:?]
	at <js>.TariffCalculator(/etc/openhab/automation/js/node_modules/openhab-spot-price-optimizer/tariff-calculator.js:33) ~[?:?]
	at <js>.delayedFunction(power.js:141) ~[?:?]
	at <js>.r(@openhab-globals.js:2) ~[?:?]
	at com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invoke(PolyglotFunctionProxyHandler.java:154) ~[?:?]
	at jdk.proxy1.$Proxy5206.run(Unknown Source) ~[?:?]
	at org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.lambda$0(ThreadsafeTimers.java:90) ~[?:?]
	at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$12(SchedulerImpl.java:189) ~[?:?]
	at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$1(SchedulerImpl.java:88) ~[?:?]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) [?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) [?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [?:?]
	at java.lang.Thread.run(Thread.java:840) [?:?]

Here is the script code I am using

rules.JSRule({
    name: "Calculate distribution and total energy price",
    description: "",
    triggers: [triggers.ChannelEventTrigger('entsoe:day-ahead:norway:prices-received')],
    execute: (event) => {
        // Load module and create service.
        var {TariffCalculator} = require('openhab-spot-price-optimizer/tariff-calculator.js');

        // Price window for distribution and total price calculations. Use 01:00 if you are on EET.
        var start = time.toZDT('01:00');
        var end = start.plusDays(1);

        // Define your price items here.
        var priceItems = {
            spotItem: items.getItem('NordPool_EnergyPrice'),
            distributionItem: items.getItem('NordPool_Distribution_Price'),
            totalItem: items.getItem('NordPool_Total_Price')
        };

        // Define your tariffs here. From NTE bill, NOK/kWH
        var priceParams = {
            price1: 0.02838,
            price2: 0.01412,
            tax: 0.02850 
        };

        // Define your transfer product (seasonal or night)
        var product = "night";

        // Define the delay (seconds) to ensure spotPrices have been saved first.
        var delay = 30;
        console.log("Start: " + start.toString());
        console.log("End: " + end.toString());

        // Calculate distribution and total prices and save them to database.
        var delayedFunction = function () {
            var tariffCalculator = new TariffCalculator(start, end, product, priceParams, priceItems);
            var ts1 = tariffCalculator.getDistributionPrices();
            var ts2 = tariffCalculator.getTotalPrices();
            priceItems.distributionItem.persistence.persist(ts1);
            priceItems.totalItem.persistence.persist(ts2);
        };

        // Create a timer that calls delayedFucntion after the delay.
        actions.ScriptExecution.createTimer(time.toZDT().plusSeconds(delay), delayedFunction);
    },
    tags: ["JSRule", "Power"],
});

Same issue both on OH 4.2.2 and 4.3.0. Npm confirms I am using openhab-spot-price-optimizer@4.0.2. Tried both influxdb and inmemory persist for NordPool_Distribution_Price and NordPool_Total_Price, but that did not change anything. NordPool_EnergyPrice is stored in influxdb and populated by the the Entso-E binding, and is currently being used by the optimizer rules with great success.

Hi, thanks for the bug report!

Based on the error message the problem is on this line:

By just looking at my code, it looks incorrect. I should be passing this.start and this.end instead of start and end. I’ll investigate this a bit further but if my visual inspection is correct, I don’t understand how the code can ever have worked either in my DEV environment (docker) or in my PROD environment (openhabian).

I’ll investigate this and will get back to you soon!

Thanks again for reporting this bug!

Cheers,
Markus