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

It’s most probably something else than the semantic class that you changed that fixed your issue. But glad that you got it working.

Markus

Finally I have managed to get the SPO working in our test environment. However I have not been able to configure the Heat Pump Compressor Optimizer to work as needed in our heating system.

In our current old verson system the heat pump needed hours are calculated based on temperature and a slight modification in the rule forces the needed hours in InfluxDB to be between 0.00 and 8.00 where the cheapest hours usually are. During this period our 1 900 L storage tank is heated.

The reason for this is that we have a battery in our solar system and if the heatpump would start at random times during the day it would quickly drain our battery. During the winter the battery only charges during the night and in the summer the heat pump doesn’t run at all.

The new optimizer is full with different setting possibilities but they are propably designed for a heat pump that can run at any moment during the day with low electricity price. At least I have not been able to find a setting where the heating need is calculated for the whole day but the heating occures between 0.00 and 8.00. I tried a restricted time period similar to what can be done with the Boiler Optimizer rule but then you get only a part of the needed heating hours. Can our needs be met at all without modifications?

There also seems to be some kind of problem (OS related?) with the datetime selector in the Control Parameters page,

  • on an iMac OSX in Safari it is shown but not working
  • on an iMac running Windows 11 in Paralells Desktop in both Firefox and Edge it is shown in two different selection layouts (have not tested working though)
  • on an iPad IOS in both Safari and Chrome the selector is not shown at all

Is there any solution for this?

Rainer

Did you try to use start & end as 00:00 and 08:00 and use just 1 heating period? That would allocate all of the heating need between this time.

And then you can use GenericOptimizer for 08:00 and 00:00 next day and just block everything.

Haven’t tested this myself but on the top of my head I can’t figure out any reasons why this would not work…

Markus

I would recommend you to open a separate thread for just this topic since this is General openHAB UI thing and not specific to the coptimizer. I’m allergic to apples myself so I have not experience how they behave with openHAB UI.

I recommend to include the complete Page configuration code and screenshots of the widget in your post.

Markus

I just rerun the heat pump optimizing for tomorrow,

  • settings 1 heating period for the hole day gives a total heating time of 4,5 hours divided in two periods 0:00 to 3:00 and 22:30 to 0:00
  • settings 1 heating period between 0:00 and 8:00 gives a total heating period of 2 hours between 0:00 and 2:00

A total of 4,5 hours seems to be correct, our old system has 4 hours for tomorrow between 0:00 and 4:00.

Forcing the hours to start at 0:00 and stop at 8:00 doesn’t allocate the total 4,5 hours needed beween these times.

You should be able to replicate your old behavior as follows:

  1. Get the average temperature for tomorrow (whole day) using the openHAB persistence services. An example is a couple of comments above, as I just instructed Timo on this (the only difference is that the example was for average spot price, you want to get average temperature).

  2. You can use the Heating Calculator service which is still available, the Heating Period Optimizer calls this under the hood. See how to call this from the documentation block: openhab-spot-price-optimizer/heating-calculator.js at main · masipila/openhab-spot-price-optimizer · GitHub

  3. Then use Generic Optimizer to allow the needed amount of hours between 00-08 and blocked the rest.

Is this sufficient or does you need complete example (it will take some time for me to do that as I have practice this evening straight after work)?

Markus

Thank you Markus for your hints in the right direction - I got the Heat Pump needed hours calculation and allocation between 00:00 and 8:00 working.

Some small cleanup and confirmation testing weeks and then I set the SPO as our everyday system.

By the way, any ideas why the datetime selector doesn’t show up on IOS devices?

No, unfortunately not. See my comment 2 days ago how I would propose to proceed with that.

The code for the Control Parameters page is here just as copy pasted from your Github page.

config:
  label: Control Parameters
  sidebar: true
blocks:
  - component: oh-block
    config:
      title: Boiler & Domestic Hot Water
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-stepper-card
                      config:
                        item: BoilerHours
                        max: 24
                        min: 0
                        step: 0.25
                        title: Boiler hours
  - component: oh-block
    config:
      title: Heating
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-input-card
                      config:
                        type: datetime-local
                        inputmode: text
                        title: "Heat Pump Compressor: Force hour ON"
                        footer: =items.HeatPumpCompressorForceOn.state
                        name: HeatPumpCompressorForceOn
                        sendButton: true
                        clearButton: true
                        item: HeatPumpCompressorForceOn
  - component: oh-block
    config: {}
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-input-card
                      config:
                        type: datetime-local
                        inputmode: text
                        title: "Heat Pump Compressor: Force hour OFF"
                        footer: =items.HeatPumpCompressorForceOff.state
                        name: HeatPumpCompressorForceOff
                        sendButton: true
                        clearButton: true
                        item: HeatPumpCompressorForceOff
masonry: []
grid: []
canvas: []

When run on an iMac Mini the page looks like this but the date time control doesn’t work, item setting doesn’t get set.

And when viewed on an iPad the date time picker is not shown at all.

As I mentioned earlier, this looks like an openHAB core issue, not related to the openHAB Spot Price Optimizer.

I have no idea whatsoever how the date picker is implemented under tje hood. You are (much) more likely to get support to this date picker issue if you create a completely new thread to the forum which is specific to the datepicker thing.

Markus

After the latest IOS update the DateTime picker is now shown with the time selection shown in the Apple scrolling style and it is setting the ForceOn/Off item state.

My next issue that I have been tried to figure out is how to modify the UI Examples Page ForceON, ForceOFF and CloneControlPoints failsafe version 3 rules to rules that works in version 4 without influx.js - so far with little success.

Are you by any chance at some point going to update them for version 4?

No need for updated version 4 rules.

I finally figured out that I can modify the Boiler optimizer rule for adding an additional heating period later during the day if needed by setting the start and end times with the DateTime pickers on the ControlParameters page and add some validation for valid date and time.

Also noticed that the CloneControlPoints is already in the version 4 package.

It seems that I sometimes ask for help before I have slept over one night…

Hello, I would have a question about the usage of the Heating Period Optimizer, or also using the general optimizer:
Currently the Optimzer provides control points to switch the heatpump (or consumption) On/OFF as I understood.

Our heatpump supports the SG ready (SmartGrid ready ) inputs like shown below. And the heatpump is doing hot water generation (based on temperature control) and heating (currently also base on indoor temperature control)

Are there any means or thoughts available in the algorithm to instead of only ON/OFF control points to use a more stepwise control?

I could simply convert
OFF command to translate to “1” (forced off)
ON command to translite to “2” (normal)
But it would be smarter to use also the other steps somehow (e.g. the most cheapest ours to switch to “5” or “6”…)

Many thanks for any thoughts on that!

SG ready modes
0: Do not use
1: Forced off (equal to TB_SG1=close /TB_SG2=open)
2: Normal operation (equal to TB_SG1=open /TB_SG2=close)
3 : On recommendation (equal to TB_SG1=open /TB_SG2=close)
4 : On command (equal to TB_SG1=close /TB_SG2=open)
5 : On command step 2 (++ power consumption compared to normal)
6 : On recommendation step 1 (+ power consumption compared to normal)
7 : Energy saving mode (power consumption compared to normal)
8 : Super energy saving mode (- power consumption compared to normal)"

I am using version 3 of the spot price oprimizer. For some reason the graph for current day shows the bars as very thin lines. In addition the stack has a “ghost member” that shows up as a fully transparent bar, that I can not identify.

Graph for today:

Graph for previous days is always ok, except for the “ghost bar” that can be seen in the wee hours of the morning . Here is the graph for yesterday:

I have removed and rewritten all graph YAMLs to no avail. Any ideas where to continue debugging?

Hi @jrieder !

You’re raising excellent questions! Even though all Spot Price Optimizer examples are binary ON or OFF, it doesn’t mean that your devices need to be controlled in ON/OFF manner. You can for example let it run with it’s own internal “default” mode most of the time and then use the Generic Optimizer to change it to different mode for example for the cheapest time of the day and to another mode for the most expensive time of the day.

I haven’t tested this but this should / could be conceptually possible already now:

  • The default mode of your heat pump would could be 2 “Normal operation”. You would do this so that when there is no control point, your device falls back to this mode.
  • You could use the Generic Optimizer to find for example the 4 most expensive hours of the day and “block” them, meaning that the control points would be 0. And you would then toggle your heat pump to either mode 7 “Energy saving mode” or mode 8 “Super energy saving mode”.
  • You could use the Generict Optimizer to find for example the 4 cheapest hours of the day and “allow” them, meaning that the control points would be 1. And you would toggle your heat pump for example to mode 5 or 6 (whatever they would do in the real world, probably heat the domestic hot water more than normally or increase the target temperature of the heating).

If you would need more fine-grained modes, then you would need to use the lower level API of the GenericOptimizer, which is the setControlForPeriod method. This method allows you to write any numeric values to your database as control point values, not just 1 or 0.

The real question here is what does the “it would be smarter to also use the different modes somehow” mean in the real world. The first thing that you need to investigate is what exactly does your heat pump do when it is in mode 5 or mode 6 and what is the difference between them. And what exactly does the pump do when it is in mode 7 or 8 and what is the difference between them. Then, you can start thinking about your use cases on conceptual level, and I’m more than happy to exchange ideas and experiences regarding these.

Cheers,
Markus

This means that there are control points in your database with lesser resolution than 15 minutes.The first thing is to try pinpoint if this is happening to all of your items (heat pump compressor, polestart charging, leaf charging, dish washer, washing machine) or just one of them.

So try to toggle down all these control points in your graph (don’t modify the code, click the legend to hide it temporarily). Then enable the items one by one to see if you get the “normal” thicker bars (that happens when you have data with 15 min resolution) or if you get thinner bars (that happens when you have at least 1 control point which is between the normal control points that you have at 00, 15, 30 and 45 min past a full hour. You can either move your mouse carefully over the graph to see the timestamps for the control points that you have, or you can write a temporary small script that prints all control points to the log.

Once you identify which item is causing the narrow bars, you need to start thinking that what else than the optimzer rules could be persisting data to those items. It can be for example that you have define the persistence strategy to be like “on every change” or “on every update”, and that you have some other stuff that you have built for UI purposes that is writing data to your database.

Markus

A small problem occured in the system last night - the heat pump did not stop at the end of the heating period. In our setup the time window for the optimization is between 22.00 and 7.00. If the heat pump is optimized to run for example between 1.00 and 3.00 everything works fine but if the run hours are like last night 3.30 and 7.00 the heat pump does not stop at 7.00 that also is the optimization window end.

The chart in openHAB looks OK but if you look at the InfluxDB chart you can see that there are no 0 values after 7.00. In a chart where the heating period stops before 7.00 there are correct values for the whole day. The red Influx curve is the heat pump.



Last night charts - heating doesn’t stop at 7.00


Previous night OK

The code optimizing the heat pump is based on your earlier suggestions.

// CALCULATE AVERAGE TEMPERATURE ======================================================
// Define the temperature item and the optimization period low tariff 22 - 7.
var tempItem = items.getItem('FMI_Weather_Forecast_Temperature');
var start = time.toZDT('22:00');
var end = start.plusDays(1);

// Calculate the average temperature and print it to the log
var avgtemp = tempItem.persistence.averageBetween(start, end);
var avg = avgtemp.numericState.toFixed(2);
//console.log("Average temperature: " + avg);

// Update the new average temperature item which you have created
var avgItem = items.getItem('AverageTemp');
avgItem.postUpdate(avg);


// CALCULATE HEATING HOURS NEEDED =======================================================
// Load modules and create service factory.
var { HeatingCalculator } = require('openhab-spot-price-optimizer/heating-calculator.js');
var heatcalc = new HeatingCalculator();

var heatCurve = [];

// Required optimization parameters, heat curve can have 2 or more points.
heatCurve = [                     // Heat curve:
  {temperature : -25, hours: 8},  // - Average temperature -25°C, heat 24h / day
  {temperature : 2, hours: 5},    // - Average temperature  +2°C, heat  7h / day 
  {temperature : 13, hours: 1}    // - Average temperature +13°C, heat  2h / day
];

// Read the average day temperature from the AverageTemp item.
var avgTempItem = items.getItem('AverageTemp');
var temperature = avgTempItem.numericState;

var numberOfPeriods = 1;   // Number of heating periods in a day, 1 = 24h.
var hours = 0;

// Calculate the heating hours needed.
hours = heatcalc.calculateHeatingHoursLinear(heatCurve, temperature, numberOfPeriods);
hours = hours.toFixed(2);

// Modify the hours with the Heating Need Adjustment value
var adjItem = items.getItem("HeatingNeedAdjustment");
var adjHours = parseFloat(adjItem.numericState);
hours = +hours + +adjHours;  // The + before the variable names ensures they are treated as numbers! 
//console.log("Adjustment: " + hours);

// Update the new heating hours item which you have created
var avgItem = items.getItem('HeatingHours');
avgItem.postUpdate(hours);

// CREATE OPTIMAL CONTROL POINTS FOR THE HEATING HOURS ================================
// Load module and create service.
var { GenericOptimizer } = require('openhab-spot-price-optimizer/generic-optimizer.js');
var optimizer = new GenericOptimizer();

// Define price item and control item here.
var priceItem = items.getItem('TotalPrice');
var controlItem = items.getItem('HeatPumpCompressorControl'); 

// Read how many heating hours are needed from the HeatingHours item.
var item = items.getItem("HeatingHours");
var hours = parseFloat(item.numericState);

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

// Define the delay (seconds) to ensure prices have been saved first.
var delay = 60;

// Read prices from the database, optimize and save the control points.
var delayedFunction = function() {
  var prices = priceItem.persistence.getAllStatesBetween(start, end);
  optimizer.setPrices(prices);
//  optimizer.allowPeriod(hours);
  optimizer.allowIndividualHours(hours);
  optimizer.blockAllRemaining();
  var timeseries = optimizer.getControlPoints();
  controlItem.persistence.persist(timeseries);
};

// Create a timer that calls delayedFucntion after the delay.
actions.ScriptExecution.createTimer(time.toZDT().plusSeconds(delay), delayedFunction);

Bye the way, the same problem also occures with the boiler if the heating period ends at the optimizing window time. Any ideas how to fix this issue?

Create another rule action / modify your existing rule which blocks everything between 07 and 22.

You can do this using GenericOptimizer, using 07-22 as the window and using the blockAllRemaining method.

This will guarantee that you have control points 1 or 0 for the whole day. First created by your existing rule and the rest by this “block all” rule.

Markus

Thx once again Markus. Modified the rule according to your suggestions and temporarely modified the end and start times to re-run last nights optimization. Now the InfluxDB got 0 values for the 7.00 to 22.00 time window.

Rainer

Hi

Super nice work as before, I updated to the newest relaease today and have openHab 4.3.4 with Entsoe-E binding and FMI binding installed and configured. I get data in influx db from them but whe i try to execute generic optimizer I get the following error measseages. 1 is the amount of hours to be active between the two following timestamps, that is set by an item. As far as I understand the generic-optimizer.js this error comes when you have les than two points in the spotprice item, however I have data for today and tomorrow. Doesn anyone have any idea what could have been going wrong?

16:47:59.794 [INFO ] [enhab.automation.script.ui.df884217b9] - 1
16:47:59.800 [INFO ] [enhab.automation.script.ui.df884217b9] - 2025-04-14T12:37:32.302+02:00[Europe/Stockholm]
16:47:59.806 [INFO ] [enhab.automation.script.ui.df884217b9] - 2025-04-14T20:47:59.801+02:00[Europe/Stockholm]
16:47:59.914 [ERROR] [enhab.automation.script.ui.df884217b9] - generic-optimizer.js: Not enough prices for optimizations!
16:47:59.915 [ERROR] [enhab.automation.script.ui.df884217b9] - generic-optimizer.js: Aborting optimization, see previous errors!
16:47:59.916 [INFO ] [enhab.automation.script.ui.df884217b9] - generic-optimizer.js: block all remaining...
16:47:59.916 [ERROR] [enhab.automation.script.ui.df884217b9] - generic-optimizer.js: Aborting optimization, see previous errors!
16:47:59.917 [ERROR] [enhab.automation.script.ui.df884217b9] - generic-optimizer.js: Aborting optimization, see previous errors!

Part of rule that have been edited to get start and possible end time from item.

// Read how many hours are needed from the BoilerHours item.
var item = items.getItem('EV2_estimated_charging');
var hours = parseFloat(item.numericState);
console.log(hours)
// Define the optimization window here, this example optimizes tomorrow.
var startItem = items.getItem("Evconnected");
var start = time.toZDT(startItem);
console.log(start);
var end = time.toZDT().plusHours(4);