4.x ‐ Usage example: Heating period optimizer - masipila/openhab-spot-price-optimizer GitHub Wiki
Important note
- These instructions apply only to version 4.x of openHAB Spot Price Optimizer.
- See separate wiki page for version 3.x of openHAB Spot Price Optimizer.
Overview
This documentation page gives an example how to use the HeatingPeriodOptimizer
class to optimize the heating of a house.
Optimizing the heating of the house has multiple objectives, which need to be balanced:
- Optimizing the heating to the cheapest time of the day
- Ensuring that the house won't cool too much during the day
- Additionally, you might want to minimize the number of compressor starts if you have an on/off ground source heat pump.
Pre-requisites
- Spot prices are available in your database. You can the Binding of your choice, as long as the price data is persisted as a proper
forecast
timeseries. See an example how to fetch spot prices using the Entso-E Binding. - Weather forecast is available in your databased. You can use the Binding of your choice, as long as the weather data is persisted as a proper
forecast
timeseries. See an example how to fetch weather forecast using the FMI Weather Binding. FMI provides weather forecast for Scandinavia.
Conceptual description of the optimization steps
Let's use 12 January 2024 as an example day. In the figure below, the blue area represents total electricity price (spot price + tariff) and the green line presents the temperature in the weather forecast. The yellow bars represent the result of the heating optimization. As you can see, there is quite steep temperature drop from -4 to -18 degrees during the day.
Step 1: Calculation of the heating needs
The algorithm first divides the day into heating periods and calculates the heating needs for each period based on a heating curve, which defines how many hours the heating needs to be ON on different temperatures. Because buildings are different, the optimization parameters are highly configurable. In this example, we divide the day into 4 x 6h heating periods and use a heating curve defined by two points:
- When the average temperature of the day is -25, heating needs to be ON for 24 hours per day
- When the average temperature of the day is 13, heating needs to be ON for 0 hours per day.
Version 4.x of openHAB Spot Price Optimizer introduced support for multiple points in the heat curve. It is now possible to have for example a curve which is less steep when it's warmer by defining three points as follows:
- When the average temperature of the day is -25, heating needs to be ON for 24 hours per day
- When the average temperature of the day is +2, heating needs to be ON for 7 hours per day
- When the average temperature of the day is +13, heating needs to be ON for 0 hours per day.
The algorithm considers 50% of each period's heating need to be flexible and the rest non-flexible. The idea here is to ensure that each heating period will have at least some minimum amount of heating, the non-flexible part. The flexibility is configurable in two ways:
- The default flexibility, 50% in this example, is configurable between 0% and 100%.
- There is a threshold in hours for warmer times of the year after which the whole heating need will be considered flexible.
On this example days, the average temperatures and heating needs for the 4 x 6h periods are as follows:
- Period 1, 00:00 - 06:00: Avg temperature -9.75, heating need 3.59 hours
- Period 2, 06:00 - 12:00: Avg temperature -5.92, heating need 2.99 hours
- Period 3, 12:00 - 18:00: Avg temperature -5.33, heating need 2.89 hours
- Period 4, 18:00 - 00:00: Avg temperature -11.78, heating need 3.91 hours
Step 2: Adjust the heating needs
As we can see, there is a big temperature drop in the evening as the average temperature drops to -11.78 degrees. What is not visible in the picture is that it's getting even colder when the day changes, the average temperature between 00:00 - 06:00 of the next day is -16.83 degrees.
To ensure enough heating, the algorithm compares the average temperatures between the heating periods and when it detects a temperature drop of 2 degrees (configurable), the heating need and flexibility for each period is adjusted.
- If there is a temperature drop between three consecutive heating periods (A to B is dropping by at least 2 degrees and B to C is dropping at least 2 degrees), the flexibility is set to 0 for all three affected heating periods A, B and C. Furthermore, the heating need of period A is updated to the original heating need of B, and heating need of period B is updated to the original value of heating need C. In other words, the algorithm increases heating on periods A and B to prepare for the temperature drop.
- If there is a temperature drop between just two consecutive heating periods, then the flexibility is set to 0 for both heating periods.
After these adjustments, the heating needs for our example day are as follows:
- Period 1, 00:00 - 06:00: avg temperature -9.75, heating need 3.59, flexibility: 0.5
- Period 2, 06:00 - 12:00: avg temperature -5.92, heating need 2.99, flexibility: 0.5
- Period 3, 12:00 - 18:00: avg temperature -5.33, heating need 3.91, flexibility: 0 (3.91 hours is the original need of the next period)
- Period 4, 18:00 - 00:00: avg temperature -11.78, heating need 4.71, flexibility: 0 (4.71 hours is the original need of the next period after midnight)
In order to do the temperature drop analysis for the first and last heating periods of the day, the algorithm checks also the last heating period of yesterday and the next 2 heating periods of tomorrow. The latter point assumes that the weather forecast contains enough data to the future.
Optional: Heating need adjustment with a parameter
The version 4.x of the openHAB Spot Price Optimizer introduces a new optional parameter which allows small adjustments to the heating needs. This is handy if you are for example going for a vacation and want to drop the temperature slightly. Previously this would have required adjustment of the heat curve, but now it's possible to have an Item called HeatingNeedAdjustment
which allows you to increase or decrease the heating need. The adjustment is done after the heating needs have been calculated based on the heat curve but before the temperature drop compensations are done.
Create a new Item called HeatingNeedAdjustment
as illustrated in the screenshot below.
- Set the type to be Number
- Set the Category to be Temperature
- Leave the dimension empty
The unit of measure of this adjustment item is hours / day.
- Let's assume the day is divided into 4 x 6h heating periods like we have been discussing so far.
- Let's assume the total heating need for all heating periods is 13.5 hours
- Let's assume the value of
heatingNeedAdjustment
is -2. This would mean that 2/4 = 0.5 hours will decreased from each heating period and the new total heating need for the day is 11.5 hours.
The value of this adjustment Item can easily be changed with a stepper card UI widget. See an example on the separate UI example documentation page
Step 3: Allocation of the non-flexible heating need
Once the heating needs have been determined, the algorithm first allocates the non-flexible heating needs for each heating period in 15 minute increments by finding the cheapest 15 minute slots. The heating need is rounded up to the closest 15 minute.
- Period 1: allow 2h in slots of 15 mins
- Period 2: allow 1h 30 min in slots of 15 mins
- Period 3: allow 4h in slots of 15 mins
- Period 4: allow 4h 45 min in slots of 15 mins
Step 4: Allocation of the flexible need
After the non-flexible heating needs have been allocated, the algorithm allocates the flexible heating need.
On this example day the remaining, flexible heating need is 3h 30 minutes. The algorithm searches the cheapest 15 minute slots from the whole remaining day and allows heating for them.
After the non-flexible and flexible heating needs have been allocated, the remaining 15 minute slots are blocked.
Step 5: Minimizing the compressor starts
As mentioned in the beginning of the page, one of the optimization objectives is to minimize the compressor starts of traditional ON/OFF ground source heat pumps. According to some sources, the compressors tend to break after 100 000 starts, so it is not desired to have it running it short 15 minute periods, then stop for 15 minutes, only to start again for 15 minutes.
On this example day, there is only one short gap at 19:45 as illustrated in the figure below.
The algorithm has a couple of more optional configuration parameters:
- Threshold for minimum heating time: I use 0.5 hours as the minimum heating time. If there is a shorter heating time than this, the algorithm will shift this heating time either left or right, depending which is cheaper. The minimum heating time threshold is configurable and can be omitted if short heating times is not a concern.
- Threshold for short gaps: I use 1 hour as this threshold. If there is a heating gap which is 1 hour or shorter, the heating periods around this gap will be merged.
- For both shifts (avoid short heating, avoid short break), there is a configurable price limit which prevents the merging from happening if the price difference is higher than the limit. The purpose of this limit is to prevent merging happening if there is for example a 1h break in the heating for the highest price peak of the day.
Create an item 'HeatPumpCompressorControl'
- The yellow bars in the example above are the control points for when the heat pump compressor should be on. These control points are calculated by the script below and stored to the database as an Item
HeatPumpCompressorControl
. So let's create an Item with this name. - The type of this Item must be Number.
- See an example of control point visualization chart that renders control points with spot prices
Configure the persistence strategy of your control item
- The
HeatingPeriodOptimizer
will prepare a timeseries of control points for the Item you created in the previous step. - The persistence strategy of this Item needs to be configured to be
forecast
, and only that, because you will have timestamps in the future. - In other words, the control item MUST NOT be persisted for example using
everyChange
,everyUpdate
,restoreOnStartup
or any other persistence policy. If you have a persistence policy for one of these strategies, you MUST exclude the control item from that strategy. - The recommended approach is to use an Item Group called
AllForecastItems
and configure the persistence policies for the whole group. If you follow this recommendation, all you need to do is to add the control Item to theAllForecastItems
group. - Instructions for creating the group and configuring the the persistence policy for the whole group is documented on the EntsoE instruction page.
Create a Rule HeatPumpCompressorOptimizer
- Create a new Rule
HeatPumpCompressorOptimizer
as illustrated in the screenshot below. - The example Rule has two Triggers:
- It is triggered after the Entsoe-E Binding has fetched new spot prices for tomorrow
- Optional: It is also triggered if you change the value of the item
HeatingNeedAdjustment
. This optional configuration is discussed above in step 2 and is only available in the version 4.x of openHAB Spot Price Optimizer. - If you want, you can also add more triggers, for example a time based trigger to re-optimize the control points in the evening (the weather forecast may change during the day).
- Select ECMAScript as the scripting language (ECMAScript 262 Edition 11)
- Copy the following script as the Script Action and modify the parameters to meet your needs.
Required parameters
- First define the names of your price item, weather forecast item and the name of the item that contains the control points.
- The number of heating periods per day (4 x 6h in this example) and the heat curve are the only required parameters.
- The heat curve must contain at least two points, but you can add more if your heating need is not linear.
periodOverlap
Optional parameter: If you want to give more weight on the price optimization, you can allow the heating periods to overlap for example by 1 hour. With 4 x 6h heating periods with 1 hour overlap, the cheapest prices for the non-flexible heating need would be searched from these time windows:
- Period 1, 00:00 - 06:00
- Period 2, 05:00 - 13:00
- Period 3, 11:00 - 19:00
- Period 4, 17:00 - 00:00
dropThreshold
Optional parameter: - Threshold for what is considered a "drop" in average temperatures.
- See conceptual description above
flexDefault
and flexThreshold
Optional parameters: - The
flexDefault
defines how much of the heating need of each period is considered flexible. The remaining part of the heating need is considered non-flexible. - If the
flexDefault
is for example 0.5 (50%) and the heating need of a 6h period would be 1.5 hours, then 0.75 hours (45 minutes) would be guaranteed to take place during this period. The remaining part (the other 45 minutes) will be optimized to the cheapest time of the day, which may be during another period. - If there is only a small amount of heating need for the period, it might be desired to let this heating need be 100% flexible i.e. the heating can be completely moved to the cheaper period. This is often the case in well-insulated houses during late spring and early fall when the total heating need is only a few hours and can all take place for example the night at one go. The
flexThreshold
parameter defines what is considered "small amount of heating need".
shortThreshold
, gapThreshold
and shiftPriceLimit
Optional parameters shortThreshold
defines what is the shortest possible heating, in hours. If the limit is set for example 0.5 and there would be a 15 minute heating, it would be merged either to the left or to the right so that the previous / next heating will be 15 minute longer. The purpose of this parameter is to prevent constant ON/OFF toggles for traditional ground source heat pumps so that the compressor does not wear out unnecessarily. Inverter heat pumps which are always ON and only control their speed do not necessarily need this.gapThreshold
defines what is considered too short gap between two heatings, in hours. If the limit is for example 1 h and there would be a 45 min gap between two heatings, then these two heatings would be merged together so that there is no gap between them. The rationale is to minimize the compressor starts as described above withshortThreshold
.- Both
shortTreshold
andgapThreshold
respect theshiftPriceLimit
parameter. While merging the heatings usually make sense, it might not make sense if the merging would mean that heating would be ON during the highest price peak of the day. This parameter defines the limit how much price loss is accepted so that the merging is still allowed. heatingNeedAdjustment
allows to temporarily increase or decrease the heating need (in hours / day) compared to what is calculated based on the heat curve. If the value is for example -2 and the day is divided into 4 x 6h periods, then each of the 4 periods would get -2/4 = 0.5 hours less heating need compared to what was calculated based on the heat curve. This is useful for example during vacations when the indoor temperature can be slightly colder than normal. In this example the value for this heating need adjustment is read from an Item, which can be updated from a nice Control parameters user interface.
Delay
- This example has a 90 seconds delay before the actual optimization is done. The purpose of this delay is to ensure that the prices have been saved to your database before the optimization is executed. This is needed because the rule gets invoked immediately after the EntsoE Binding's
prices-received
channel fires and the process for persisting the prices to your database might not be completed yet at that moment.
// 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"; // Name of the price item for optimizing.
parameters.forecastItem = "FMI_Weather_Forecast_Temperature"; // Name of the weather forecast item.
parameters.controlItem = "HeatPumpCompressorControl"; // Name of the control point item.
// Required optimization parameters, heat curve can have 2 or more points.
parameters.numberOfPeriods = 4; // Number of heating periods in a day.
parameters.heatCurve = [ // Heat curve:
{temperature : -25, hours: 24}, // - Average temperature -25°C, heat 24h / day
{temperature : 2, hours: 7}, // - Average temperature +2°C, heat 7h / day
{temperature : 13, hours: 2} // - Average temperature +13°C, heat 2h / day
];
// Optional parameters, remove if you don't want to use these advanced features.
parameters.periodOverlap = 1; // Number of hours that the optimization periods are allowed to overlap.
parameters.dropThreshold = 2; // Threshold for significant temperature drops.
parameters.flexDefault = 0.5; // Default amount of flexbile heating need. 0 = 0%, 1 = 100%.
parameters.flexThreshold = 1; // Threshold (in hours) to consider heating need 100% flexible.
parameters.shortThreshold = 0.5; // Minimun duration for heating (h). Shorter periods will be merged together.
parameters.gapThreshold = 1; // Merge two heating periods together if the gap between them is less than this (h).
parameters.shiftPriceLimit = 2; // Allow heating period shifting only if the price difference is less than this.
// Optional heating need adjustment, read from an item.
parameters.heatingNeedAdjustment = items.getItem('HeatingNeedAdjustment').numericState;
// Define the optimization window here, this example optimizes tomorrow.
var start = time.toZDT('00:00').plusDays(1);
var end = start.plusDays(1);
// Define the delay (seconds) to ensure prices have been saved first.
var delay = 90;
// Read prices from the database, optimize and save the control points.
var delayedFunction = function() {
var heatingPeriodOptimizer = new HeatingPeriodOptimizer(start, end, parameters, serviceFactory);
heatingPeriodOptimizer.optimize();
var timeseries = heatingPeriodOptimizer.getControlPoints();
var controlItem = items.getItem(parameters.controlItem);
controlItem.persistence.persist(timeseries);
};
// Create a timer that calls delayedFucntion after the delay.
actions.ScriptExecution.createTimer(time.toZDT().plusSeconds(delay), delayedFunction);
Create a Rule 'HeatPumpCompressorController' to toggle the compressor ON and OFF
- This rule will run every 15 minutes and turn the compressor ON or OFF based on the current control point
- If the compressor is currently OFF and the current control point is 1, the compressor will be turned ON and vice versa.
- This example script has a failsafe mechanism: If the persistence service (database) is unavailable, the device will be turned ON.
Inline script action for the rule
// Define your item names here.
var controlItem = items.getItem("HeatPumpCompressorControl");
var powerItem = items.getItem("HeatPumpCompressor");
// Get the latest control point from the persistence service.
var controlPoint = controlItem.persistence.previousState();
// Send the commands if state change is needed.
if (controlPoint && powerItem.state == "ON" && controlPoint.numericState == 0) {
console.log("Send OFF");
powerItem.sendCommand("OFF");
}
else if (controlPoint && powerItem.state == "OFF" && controlPoint.numericState == 1) {
console.log("Send ON");
powerItem.sendCommand("ON");
}
else if (controlPoint) {
console.log("No state change needed");
}
// Failsafe: if persistence service is unavailable, turn the device ON.
else {
console.warn("Persistence service returned NULL. Failsafe ON!");
powerItem.sendCommand("ON");
}
Analyzing the optimization results
Using the logs to understand the results
As this documentation page illustrates, the optimization algorithm is quite advanced and has multiple steps. Therefore it is not always obvious at first glance why the heating is ON or OFF for some individual time slot. Below is an example output of the logs for the same day which is discussed in this documentation. Note that some timestamps are on local time zone and some timestamps are on UTC. The letter Z means that the timestamp is on UTC.
heating-period-optimizer.js: Starting heating period optimizer...
-----------------------------------------------------------------
generic-optimizer.js: price window 2024-01-11T22:00Z - 2024-01-12T22:00Z (PT24H)
heating-period-optimizer.js: Calculating heating need for the heating periods.
heating-period.js: 2024-01-11T18:00+02:00[SYSTEM]: temperature -9.18, heating hours 3.50, flexibility: 0.5
heating-period.js: 2024-01-12T00:00+02:00[SYSTEM]: temperature -9.75, heating hours 3.59, flexibility: 0.5
heating-period.js: 2024-01-12T06:00+02:00[SYSTEM]: temperature -5.92, heating hours 2.99, flexibility: 0.5
heating-period.js: 2024-01-12T12:00+02:00[SYSTEM]: temperature -5.33, heating hours 2.89, flexibility: 0.5
heating-period.js: 2024-01-12T18:00+02:00[SYSTEM]: temperature -11.78, heating hours 3.91, flexibility: 0.5
heating-period.js: 2024-01-13T00:00+02:00[SYSTEM]: temperature -16.83, heating hours 4.71, flexibility: 0.5
heating-period.js: 2024-01-13T06:00+02:00[SYSTEM]: temperature -11.33, heating hours 3.84, flexibility: 0.5
heating-period-optimizer.js: Checking for significant temperature drops...
heating-period-optimizer.js: Big temperature drop detected after period starting at 2024-01-12T12:00+02:00[SYSTEM], adjusting heating hours and flexibility of this and next two periods.
heating-period-optimizer.js: Temperature drop detected after period starting at 2024-01-12T18:00+02:00[SYSTEM], adjusting flexibility of this and next period.
heating-period-optimizer.js: Heating periods after temperature drop compensations:
heating-period.js: 2024-01-11T18:00+02:00[SYSTEM]: temperature -9.18, heating hours 3.50, flexibility: 0.5
heating-period.js: 2024-01-12T00:00+02:00[SYSTEM]: temperature -9.75, heating hours 3.59, flexibility: 0.5
heating-period.js: 2024-01-12T06:00+02:00[SYSTEM]: temperature -5.92, heating hours 2.99, flexibility: 0.5
heating-period.js: 2024-01-12T12:00+02:00[SYSTEM]: temperature -5.33, heating hours 3.91, flexibility: 0
heating-period.js: 2024-01-12T18:00+02:00[SYSTEM]: temperature -11.78, heating hours 4.71, flexibility: 0
heating-period.js: 2024-01-13T00:00+02:00[SYSTEM]: temperature -16.83, heating hours 4.71, flexibility: 0
heating-period.js: 2024-01-13T06:00+02:00[SYSTEM]: temperature -11.33, heating hours 3.84, flexibility: 0.5
heating-period-optimizer.js: Optimizing the non-flexible heating need.
generic-optimizer.js: allow PT2H in slots of PT15M between 2024-01-12T00:00+02:00[SYSTEM] and 2024-01-12T07:00+02:00[SYSTEM]
generic-optimizer.js: allow PT1H30M in slots of PT15M between 2024-01-12T05:00+02:00[SYSTEM] and 2024-01-12T13:00+02:00[SYSTEM]
generic-optimizer.js: allow PT4H in slots of PT15M between 2024-01-12T11:00+02:00[SYSTEM] and 2024-01-12T19:00+02:00[SYSTEM]
generic-optimizer.js: allow PT4H45M in slots of PT15M between 2024-01-12T17:00+02:00[SYSTEM] and 2024-01-13T00:00+02:00[SYSTEM]
heating-period-optimizer.js: Optimizing the flexible heating need.
generic-optimizer.js: allow PT3H30M in slots of PT15M between 2024-01-12T00:00+02:00[SYSTEM] and 2024-01-13T00:00+02:00[SYSTEM]
generic-optimizer.js: block all remaining...
heating-period-optimizer.js: Checking if there are too short heating periods...
heating-period-optimizer.js: Checking if there are short gaps...
heating-period-optimizer.js: PT15M gap starting at 2024-01-12T17:45Z
heating-period-optimizer.js: Shift heating period to the right.
heating-period-optimizer.js: Checking if there are short gaps...
influx.js: Preparing to write 96 points to the database for HeatPumpCompressorControl
Further debugging and analyzing tips
If you want to analyze the algorithm behavior deeper, the second step after log analysis is to disable the short periods and gap shifting. The easiest way to do this is to set the shiftPriceLimit
parameter to 0, which prevents the shifting from taking place.
Discussion about the algorithm
Discussion about this algorithm is more than welcome on this thread at the openHAB community forum.
However, I would appreciate if you would show some effort before asking questions like "why is this heating on during this expensive hour" by analyzing the logs before asking the question. It is possible that the algorithm has bugs, but the code is well isolated and there is covered by 150+ automated test cases at the time this wiki page is written.