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

I finally had time to start the migration to the npm based version. As we have an eCar in our household I changed “Boiler” to “Charging” everywhere. Now the charging is toggled on/off every 15 minutes if the markets so decide. I am not sure this is very healthy for the car battery. How have you arranged your car charging with the 15 minute price changes coming up?

Use the GenericOptimizer and allowPeriod method. It takes one input argument N, where N is the number of hours you want to allow. It finds the cheapest N hour consecutive (yhtäjaksoinen in Finnish) period and allows that.

Then use the blockAllRemaining method to block all other slots.

The actual control script runs every 15 mins but it won’t go on/off back and forth if the control values are consecutive.

I can add our car charging script later today to the wiki.

1 Like

Here’s the Rule Script Action for our car charging optimization.

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
GenericOptimizer = require('openhab-spot-price-optimizer/generic-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new GenericOptimizer.GenericOptimizer('PT15M');

start = time.toZDT('21:00');
stop = time.toZDT('06:00').plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);

// Read how many hours are needed from the CarChargingHours item.
item = items.getItem("CarChargingHours");
hours = Math.round(item.state);

// Optimize the control points and save them to the database.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('car_charging_control', points);

// The rest of the day does not have any control points yet.
// Read the spot prices from the database and block them all.
start = time.toZDT('06:00').plusDays(1);
stop = time.toZDT('00:00').plusDays(2);
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('car_charging_control', points);

Conceptual description:

  • I have an item “CarChargingHours” which I can use to select how many hours I want to charge the car between 21 and 06. The math is quite easy, because 1 hour of charging time equals to a bit more than 10% increase in the charge level. So if the car is for example at 40% and I want to charge it to 80%, I would select 4 with the user interface in the screenshot below.
  • The actual rule above is executed when the “CarChargingHours” item change (and also after the spot prices have been fetched).
  • The rule finds the cheapest consecutive period between 21-06 and allows that + blocks the remaining between 21-06.
  • Finally, it blocks everything from 06 onwards until next midnight.

How to get the start and end times like in the image above?

Do you mean the “force the hour on” and “force the hour off” items?

These two UI elements allow me to manually set a given hour’s control points to “allowed” or “blocked”. Point being that if I come home for example on Saturday at 13:00 and I have to charge the car for 2 hours before I leave again at 19:00, I can manually check the prices and select the two hours when I want the car to charge.

  1. Create Items called CarChargingForcedOn and CarChargingForcedOn

  1. The code for the whole “Control parameters” page is here
config:
  label: "Ohjausparametrit: Auto"
  sidebar: false
blocks:
  - component: oh-block
    config:
      title: Auton lataus
    slots:
      default:
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-stepper-card
                      config:
                        item: CarChargingHours
                        large: true
                        max: 10
                        min: 0
                        raised: true
                        step: 1
                        title: Auton lataustunnit (21-06)
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-input-card
                      config:
                        clearButton: true
                        footer: =items.CarChargingForcedOn.state
                        inputmode: text
                        item: CarChargingForcedOn
                        name: CarChargingForcedOn
                        sendButton: true
                        title: Auton lataustunnin päällepakotus
                        type: datetime-local
        - component: oh-grid-row
          config: {}
          slots:
            default:
              - component: oh-grid-col
                config: {}
                slots:
                  default:
                    - component: oh-input-card
                      config:
                        clearButton: true
                        footer: =items.CarChargingForcedOff.state
                        inputmode: text
                        item: CarChargingForcedOff
                        name: CarChargingForcedOff
                        sendButton: true
                        title: Auton lataustunnin poispakotus
                        type: datetime-local
masonry: null
grid: []
canvas: []
  1. Here’s the code for the Rule that sets the Control points to value 1 for the selected hours (when the item CarChargingForcedOn changes)
// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');

// Create objects.
influx = new Influx.Influx();

// Read the hour that should be forced ON.
item = items.getItem("CarChargingForcedOn");
datetime = item.state;

// Prepare the control points for each 15 mins of the given hour.
points = [];
for (i=0; i < 4; i++) {
  resolution = time.Duration.parse('PT15M');
  point = {
    datetime: time.toZDT(datetime).plus(resolution.multipliedBy(i)),
    value: 1
  }
  points.push(point);
}

// Write the control point to the database.
measurement = 'car_charging_control';
influx.writePoints(measurement, points);
1 Like

For some reason I am not able to get the spot prices for the car charging control. Heat Pump control works fine.
I have not been able to track this problem down. Logs below:

your code goes here
```2024-04-13 22:33:37.122 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entsoe.js: Making an API call to Entso-E API for time interval 2024-04-11T22:00:00Z/2024-04-14T22:00:00Z
2024-04-13 22:33:37.880 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entose.js: transforming XML to JSON and parsing prices...
2024-04-13 22:33:37.979 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entsoe.js: Received time series for 3 days.
2024-04-13 22:33:37.999 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entsoe.js: Time series start: 2024-04-11T22:00Z, resolution: PT1H
2024-04-13 22:33:38.513 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entsoe.js: Time series start: 2024-04-12T22:00Z, resolution: PT1H
2024-04-13 22:33:38.969 [INFO ] [nhab.automation.script.ui.dfa34a1333] - entsoe.js: Time series start: 2024-04-13T22:00Z, resolution: PT1H
2024-04-13 22:33:39.415 [INFO ] [nhab.automation.script.ui.dfa34a1333] - influx.js: Preparing to write 288 points to the database for SpotPrice
2024-04-13 22:33:44.662 [WARN ] [tomation.script.ui.ChargingOptimizer] - influx.js: query did not return any data!
2024-04-13 22:33:44.703 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Not enough prices for optimizations!
2024-04-13 22:33:44.707 [INFO ] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Allowing the cheapest PT3H period...
2024-04-13 22:33:44.709 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:44.711 [INFO ] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Blocking all remaining slots...
2024-04-13 22:33:44.712 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:44.714 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:44.716 [ERROR] [tomation.script.ui.ChargingOptimizer] - influx.js: Unable to write points for measurement CarChargingControl, empty points received as input!
2024-04-13 22:33:44.762 [WARN ] [tomation.script.ui.ChargingOptimizer] - influx.js: query did not return any data!
2024-04-13 22:33:44.804 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Not enough prices for optimizations!
2024-04-13 22:33:44.806 [INFO ] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Blocking all remaining slots...
2024-04-13 22:33:44.807 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:44.809 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:44.811 [ERROR] [tomation.script.ui.ChargingOptimizer] - influx.js: Unable to write points for measurement CarChargingControl, empty points received as input!
2024-04-13 22:33:46.295 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - generic-optimizer.js: price window 2024-04-13T21:00Z - 2024-04-14T21:00Z
2024-04-13 22:33:47.447 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - generic-optimizer.js: Allowing the cheapest PT3H30M period...
2024-04-13 22:33:50.432 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - generic-optimizer.js: Allowing the cheapest PT3H30M period...
2024-04-13 22:33:52.997 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - generic-optimizer.js: Blocking all remaining slots...
2024-04-13 22:33:53.006 [INFO ] [cript.ui.HeatPumpCompressorOptimizer] - influx.js: Preparing to write 96 points to the database for HeatPumpCompressorControl
2024-04-13 22:33:54.865 [WARN ] [tomation.script.ui.ChargingOptimizer] - influx.js: query did not return any data!
2024-04-13 22:33:54.897 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Not enough prices for optimizations!
2024-04-13 22:33:54.900 [INFO ] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Allowing the cheapest PT3H period...
2024-04-13 22:33:54.902 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:54.903 [INFO ] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Blocking all remaining slots...
2024-04-13 22:33:54.905 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:54.907 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Aborting optimization, see previous errors!
2024-04-13 22:33:54.908 [ERROR] [tomation.script.ui.ChargingOptimizer] - influx.js: Unable to write points for measurement CarChargingControl, empty points received as input!
2024-04-13 22:33:54.950 [WARN ] [tomation.script.ui.ChargingOptimizer] - influx.js: query did not return any data!
2024-04-13 22:33:54.981 [ERROR] [tomation.script.ui.ChargingOptimizer] - generic-optimizer.js: Not enough prices for optimizations!

Please share your Rule scripts and I’ll try to find time to check tomorrow.

1 Like

ChargingOptimizer:

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
GenericOptimizer = require('openhab-spot-price-optimizer/generic-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new GenericOptimizer.GenericOptimizer('PT15M');

start = time.toZDT('21:00');
stop = time.toZDT('06:00').plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);

// Read how many hours are needed from the CarChargingHours item.
item = items.getItem("CarChargingHours");
hours = Math.round(item.state);

// Optimize the control points and save them to the database.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

// The rest of the day does not have any control points yet.
// Read the spot prices from the database and block them all.
start = time.toZDT('06:00').plusDays(1);
stop = time.toZDT('00:00').plusDays(2);
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

ChargingController:

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');

// Create objects.
influx = new Influx.Influx();

// Read the control value for the current hour from the database.
control = influx.getCurrentControl('CarChargingControl');

// ChargingPower: Send the commands if state change is needed.
ChargingPower = items.getItem("ChargingPower");
if (ChargingPower.state == "ON" && control == 0) {
  console.log("ChargingPower: Send OFF")
  ChargingPower.sendCommand('OFF');
}
else if (ChargingPower.state == "OFF" && control == 1) {
  console.log("ChargingPower: Send OFF")
  ChargingPower.sendCommand('ON');
}
else {
  console.log("ChargingPower: No state change needed")  
}

Fetch Spot Prices:

// Influx database connection parameters must be configured in config.js
// Entso-E bidding zone and authentication token must be configured in config.js

// Load modules.
Entsoe = require('openhab-spot-price-optimizer/entsoe.js');
Influx = require('openhab-spot-price-optimizer/influx.js');

// Create objects.
entsoe = new Entsoe.Entsoe();
influx = new Influx.Influx();

// Multiplier for VAT. Adjust this to your country or leave as 1.0.
vat = 1.24; 

// Fetch spot prices from yesterday 00:00 to day after tomorrow 00:00. Save them as 'SpotPrice'.
start = time.toZDT('00:00').minusDays(1);
end = time.toZDT('00:00').plusDays(2);
spotPrices = entsoe.getSpotPrices(start, end, vat);
influx.writePoints('SpotPrice', spotPrices);

In your charging optimizer, you are optimizing against ‘total_price’, which I use for spot price + seasonal grid tariff.

If you don’t calculate total_price anywhere, it will not have any data. Optimize against ‘SpotPrice’

1 Like

Thanks, now it works. Yellow is heat pump, red is car charging. I am a bit curious about the optimization code. Is it supposed to work like this? I am using your code that switches mode at 10 calculated hours of heating:

The cheaper hours just after midnight are not used. Instead the load is shifted towards the later and more expensive hours.

You can use the debug mode and follow from the logs what exactly is happening and in which order.

Go to the openHAB console and set your rule to DEBUG mode. The FetchSpotPrice rule would be set to debug mode like this, replace FetchSpotPrice with your script or rule name.

log:set DEBUG org.openhab.automation.script.ui.FetchSpotPrices

Remember to restore the normal INFO log level once your done with your investigations.

If I remeber how I set things up, given the fact I use one of the first scriptversions.

I did set up a number Item to set deadline for carchargin, most often 6am. But sometimes 03am, when I change time a script sets deadline to tomorrow at whatever time.

Then I use that item as end time in tje spotprice script, I think the original script allways used calendar dates time and I did set normal tines to 06 to 18 just to prevent a datechange interfere with charging or heating or pool heating.

Maybe that could help you or maybe I just made you more confused.

FYI for the Finnish speaking fellows…

I joined forces with the maintainer of Pörssäri and we are discussing the Ideal Heating Optimization Algorithm ™ at TechBBS. See the thread from comment 169 onwards at

Cheers,
Markus

1 Like

For tomorrows prices the code will not plan any heating at all if I set the limit to 7 hours in the code below. With 10 hours, like it is now, it plans one continuous block from midnight onwards. My other settings below:

Is this an exptect behavior?

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
PeakPeriodOptimizer = require('openhab-spot-price-optimizer/peak-period-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new PeakPeriodOptimizer.PeakPeriodOptimizer('PT15M');

//If the script is called after 14.00, optimize tomorrow. Otherwise optimize today.
start = time.toZDT('00:00');
if (time.toZDT().isBetweenTimes('14:00', '23:59')) {
  start = start.plusDays(1);    
}
stop = start.plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);

// Read the control points of the previous day and pass them for the optimizer.
previousDayStart = start.minusDays(1);
previousDayStop = start;
previousControlPoints = influx.getPoints('HeatPumpCompressorControl', previousDayStart, previousDayStop);
optimizer.setPreviousControlPoints(previousControlPoints);

// Read desired amount of heating hours from the HeatingHours item.
heatingItem = items.getItem("HeatingHours");
heatingHours = heatingItem.state;

// Read the minimum amount of hours between the blocked periods from the MidHeatingHours item.
midItem = items.getItem("MidHeatingHours");
midHeatingHours = midItem.state;

// Define how many peaks you want to block.
peaksItem = items.getItem("HeatingPeaks");
peaks = Math.round(peaksItem.state);

// If at least 10 heating hours are needed, use PeakPeriodOptimizer
if (heatingHours >= 10) {
  optimizer.setOptimizationParameters(heatingHours, midHeatingHours, peaks);
  optimizer.blockPeaks();
  optimizer.allowAllRemaining();
  points = optimizer.getControlPoints();
  influx.writePoints('nibe_control', points);
}
// Otherwise simply allow N cheapest periods. The same 'HeatingPeaks' Item is used to define the number of cheap periods.
else {
  for (i=0; i < peaks; i++) {
    optimizer.allowPeriod(heatingHours/peaks);
  }
  optimizer.blockAllRemaining();
  points = optimizer.getControlPoints();
  influx.writePoints('HeatPumpCompressorControl', points);
}

No. Did you check your logs and what are your conclusions from reading them?

I had forgot to update the heat pump item name when upgrading to the new scripts. After correcting that all works as it should

The next generation kick ass algorithm for heating the house is looking very good.

Conceptual description and a couple of simulated weeks from last winter can be found at

(Use google translate if you’re interested but don’t understand our cryptic language)

3 Likes

Our eCar does not start the cabin preheating unless charging power is connected. Therefore I would like to have the charger start every day from 07:30-08:00. With the code below the nightly charging is set, but the hours after 07:30 all have no control points and as a result the charging control script turns the charging on for the whole day. I have not found why this happens. Any ideas? Script below,

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
GenericOptimizer = require('openhab-spot-price-optimizer/generic-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new GenericOptimizer.GenericOptimizer('PT15M');

start = time.toZDT('21:00');
stop = time.toZDT('07:30').plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);

// Read how many hours are needed from the CarChargingHours item.
item = items.getItem("CarChargingHours");
hours = Math.round(item.state);

// Optimize the control points and save them to the database.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

// Set 07:30 to 08:00 ON for cabin heater to work
start = time.toZDT('07:30').plusDays(1);
stop = time.toZDT('08:00').plusDays(1);
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);
optimizer.setControlForPeriod(start, stop, '1');
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

// The rest of the day does not have any control points yet.
// Read the spot prices from the database and block them all.
start = time.toZDT('08:00').plusDays(1);
stop = time.toZDT('21:00').plusDays(2);
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

You must ensure that every 15 minute has a control point, either 0 or 1. If a control point does not exist or cannot be read, the controller will default to 1 as a failsafe. This is by design.

Here’s what I use with our car (note that my item names differ slightly from yours, so adjust this to match your item names). Note that there is a scrollbar in the code block so ensure that you see the last part all the way to the end.

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
GenericOptimizer = require('openhab-spot-price-optimizer/generic-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new GenericOptimizer.GenericOptimizer('PT15M');

start = time.toZDT('21:00');
stop = time.toZDT('07:00').plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);

// Read how many hours are needed from the CarChargingHours item.
item = items.getItem("CarChargingHours");
hours = Math.round(item.state);

// Optimize the control points and save them to the database.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('car_charging_control', points);

// The rest of the day does not have any control points yet.
// Read the spot prices from the database and block them all.
start = time.toZDT('07:00').plusDays(1);
stop = time.toZDT('00:00').plusDays(2);
prices = influx.getPoints('total_price', start, stop);
optimizer.setPrices(prices);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('car_charging_control', points);

If you want to always allow 7:30-8:00, use the setControlForPeriod method of the GenericOptimizer, see openhab-spot-price-optimizer/generic-optimizer.js at main · masipila/openhab-spot-price-optimizer · GitHub

setControlForPeriod takes three arguments as documented in the code, start, duration and control (you want to set it to 1).

So you would add something like this to your script (didn’t test this so can’t gurantee that the following snippet doesn’t have typos or brainfarts). This would be BEFORE you do points = optimizer.getControlPoints()

// Force charging ON tomorrow at 7:30 for 30 minutes.
cabinHeatingStart = time.toZDT('07:30').plusDays(1);
cabinHeatingDuration = time.Duration.parse('PT30M');
optimizer.setControlForPeriod(cabinHeatingStart, cabinHeatingDuration, 1);

Markus

1 Like

There are three instances of this in the script. I am not quite sure how to interpret this. The code below does not work, however:

// Load modules. Database connection parameters must be defined in config.js.
Influx = require('openhab-spot-price-optimizer/influx.js');
GenericOptimizer = require('openhab-spot-price-optimizer/generic-optimizer.js');

// Create objects.
influx = new Influx.Influx();
optimizer = new GenericOptimizer.GenericOptimizer('PT15M');

start = time.toZDT('21:00');
stop = time.toZDT('07:30').plusDays(1);

// Read spot prices from InfluxDB and pass them for the optimizer.
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);

// Read how many hours are needed from the CarChargingHours item.
item = items.getItem("CarChargingHours");
hours = Math.round(item.state);

// Optimize the control points and save them to the database.
optimizer.allowPeriod(hours);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);

// Force charging ON tomorrow at 7:30 for 30 minutes.
cabinHeatingStart = time.toZDT('07:30').plusDays(1);
cabinHeatingDuration = time.Duration.parse('PT30M');
optimizer.setControlForPeriod(cabinHeatingStart, cabinHeatingDuration, 1);

// The rest of the day does not have any control points yet.
// Read the spot prices from the database and block them all.
start = time.toZDT('08:00').plusDays(1);
stop = time.toZDT('00:00').plusDays(2);
prices = influx.getPoints('SpotPrice', start, stop);
optimizer.setPrices(prices);
optimizer.blockAllRemaining();
points = optimizer.getControlPoints();
influx.writePoints('CarChargingControl', points);