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

Sounds interesting… Do you want to elaborate what PV stands for so that the community can understand the possible future readers / commentators point of view?

Cheers,
Markus

Photovoltaic, producing your own power that is. When you feed the grid you get a lot less money than you save when you run your devices with that power you generated.
So the problem class is quite similar to that of this thread and so are the possible solutions (increase self consumption by moving loads into times of excess solar power).
The challenge with PV power however is that you never know upfront when there will be excess and how much.

Interesting differences between the countries… My neighbor has solar panels and they have been optimizing their home pretty much to the max already for years.

In Finland, it used to be so that it was significantly better to consume the power generated by your solar panels yourself. Our neighbors were optimizing for this in the past.

Now that the spot prices are sky high during daytime and very low during the nights, it’s MUCH more profitable for them to sell as much of the solar power they can (minimize their own consumption during daytime) and then purchase cheap electricity during the nights to heat their own water, for dish washer and laundry.

In Finland you can sell your solar power back to the grid and the price you get follows the Nordpool spot prices.

Cheers,
Markus

Indeed. Germany suffers a lot from regulatory lobbyism. There’s very few providers of variable tariffs, some do not even accept new customers at the moment, and you have to pay a high base fee per kWh for transmission so savings are way lower than they could be.
There’s a static feed-in limit and you need permission from your transmission provider to feed the grid from battery.
Things started to move but adoption is slow and changes only ever apply to new installations so not the vast majority of people.

HI

I will just confirm into big differences even if we live quite close, In sweden the government hands out extra 0,6 or 0,7 SEK per kWh, however it’s a big investment about 30-50’ EUR and that’s without battery, government has a handout for up to 50% if your lucky and they still have funding. I played with the idea of getting a battery and charge that when power is cheap but I don’t know if thats a good investment.

One more post before other activities I made a rule script that actually a copy of the waterheater script but I wan’t 12 hours on time and a control item that’s called carcharger_control, I just changed item name to write to influx and that seems to work Carcharger_control is the blu line and spot price in SEK or atleast 1/10 of EUR /kWh (static exchangerate makes it easy to comp) But I’m not sure if the script really does what I wanted it do do.

Script that executes every hour

   dh = require('kolapuuntie/date-helper.js');
        wh = require('kolapuuntie/carcharger.js');

        start = dh.getMidnight('start');
        stop = dh.getMidnight('stop');

        wh.determineHours(start, stop, 12);

The carcharger.js script that rule script exec, sorry for only changing item to write to influx and not all log lines and stuff.

/**
 * Javascript module for waterheater helper methods.
 */

/**
 * Exports.
 */
module.exports = {
    determineHours: determineHours
};

/**
 * Calculates the on/off hours for the waterheater and writes them to the database.
 *
 * @param Date start
 *   Start of the time range where to find the cheap hours.
 * @param Date stop
 *   Stop of the time range where to find the cheap hours.
 * @param int num
 *   Number of hours the waterheater must be on.
 */
function determineHours(start, stop, num) {
    console.log('waterheater.js: Starting to calculate on/off hours for the waterheater...');
    influx = require('kolapuuntie/influx.js');
    const prices = influx.getPrices(start, stop);
    const startIndex = findStartIndex(prices, num);
    const points = preparePoints(prices, startIndex, num);
    influx.writePoints('carcharger_control', points);
}

/**
 * Finds the cheapester hours for the waterheater.
 *
 * The power must be on for 'num' hours in a row so that the waterheater
 * can reach it's max temperature. Reaching the max tempereature is important
 * to avoid legionella bacteria. 
 *
 * @param array prices
 *   Array of point objects containing prices for each hour.
 * @param int num
 *   Number of hours the waterheater must be on. 
 *
 * @return int
 *   Index of 'prices' array when the heating shoud start.
 */
function findStartIndex(prices, num) {
    let cheapestSum = 0;
    let cheapestIndex = 0;

    for (let i = 0; i <= prices.length - num; i++) {
	let sum = 0;
	// Calculate the sum of the hourly prices for the current 'num' hours.
	for (let j = i; j < i + num; j++) {
	    let point = prices[j];
	    sum += point.value;
	}
	// Initial value for the cheapestSum.
	if (i == 0) {
	    cheapestSum = sum;
	}
	if (sum < cheapestSum) {
	    cheapestSum = sum;
	    cheapestIndex = i;
	}
    }
    console.log('waterheater.js: Cheapest index: ' + cheapestIndex + ', sum: ' + cheapestSum);
    return cheapestIndex;
}

/**
 * Prepares the control points to be written to the database.
 *
 * @param array prices
 *   Array of point objects.
 * @param int startIndex
 *   Index of the 'prices' array when heating shoud start.
 * @param int num
 *   Number of hours the waterheater must be on.
 *
 * @return array
 *   Array of point objects.
 */
function preparePoints(prices, startIndex, num) {
    let points = [];
    for (let i = 0; i < prices.length; i++) {
	let value = 0;
	if ((i >= startIndex) && (i < startIndex + num)) {
	    value = 1;
	}
	let point = {
	    datetime: prices[i].datetime,
	    value: value
	}
	points.push(point);
    }
    return points;
}

Hard to tell even but you can do the math and have a big excel sheet to take everything into account (battery cost, degradation, government handout, etc.).
Most of the time for variable tariffing, the answer is ‘no’ because you have to pay for transmission twice.
Remember there’s more business models a battery enables you for: you can run on free/very cheap self generated power even in times without pv production (night, winter).
You will save all the transmission cost (in and out) for any self-generated-self-consumed kWh.
If unsure or a tight decision, you might want to wait for mobile batteries a.k.a. EVs with bidirectional charging (where the ‘battery’ has more benefits than just to help with ‘hedging’).
A battery can also protect you from grid outages (for my wife that was the reason to go for it).
I’m getting off topic now.

@Marcus_Carlsson regarding your car charger question above.

I made some changes to the waterheater.js in comment #40.

If you would now grab the waterheater.js from the comment marked as the “solution”, you can use the same file for both use cases (waterheater and car charger). However, you need to make a small adjustment to the rule that you run to determine the on/off hours for the waterheater.

To summarize:

  1. Replace your current waterheater.js with the one you currently have.

  2. Update the rule action that you run in the afternoon to determine the water heater hours with this:

dh = require('kolapuuntie/date-helper.js');
wh = require('kolapuuntie/waterheater.js');
influx = require('kolapuuntie/influx.js');

start = dh.getMidnight('start');
stop = dh.getMidnight('stop');

// Determine cheap hours and write control values to the database
hours = 4;
points = wh.determineHours(start, stop, hours);
influx.writePoints('waterheater_control', points);
  1. For your car charger, you don’t need carcharger.js at all, you can create a new rule action for the car and re-use the waterheater.js like this:
dh = require('kolapuuntie/date-helper.js');
wh = require('kolapuuntie/waterheater.js');
influx = require('kolapuuntie/influx.js');

start = dh.getMidnight('start');
stop = dh.getMidnight('stop');

// Determine cheap hours and write control values to the database
hours = 12;
points = wh.determineHours(start, stop, hours);
influx.writePoints('carcharger_control', points);

Having said this, it’s once again proven that there are only two things which are difficult in IT:

  1. Finding good names
  2. Invalidating cache
  3. Offset by one

The name “waterheater.js” is a poor name for the script. A name like “FindCheapHours.js” would be much more descriptive, because all that script does is that it finds a given number of cheap hours between the start and end times.

Two remarks.

  1. You definitely do not need to run this script once per hour. The spot prices between start midnight and stop midnight do not change. So as long as you run this script in the afternoon after the next day’s prices have been stored, you should be fine. I run my script at 13.30 CEST (which would be fine if everything works) and then 14.30 CEST and 20.30 CEST just to be sure (in case the fetching of spot prices failed at 13.30 CEST).

  2. The waterheater.js script is designed to find the cheapest window of adjacent hours.

  • In other words, I want my waterheater to be on for x hours in a row.
  • I do not want it to run for one hour now, and then another one hour 12 hours later. The reason for this is that in the context of a water heater, I want to ensure that the water heater reaches the thermostat max temperature every day so that there is no risk for legionella bacteria.
  • If the waterheater is only “on” for say 1 hour, it might be that the water temperature only reaches say 42 degrees Celsius. This is not enough to kill legionella. Say that there is then hot water consumption and the temperature drops to 35. You then heat it again for 1 hour and you still would not t hit the max temperature and thus you don’t kill legionella.
  • For this reason, the waterheater.js is searching a window of x cheap hours where the hours are in a row
  • All in all: running the waterheater.js with n = 12 hours is not necessarily the most efficient for you unless your car needs to be charged at one go without interruptions.

If your car charging is OK being interrupted and then continues automatically, you can get much better results by reusing nibe.js which does not have this criteria that the hours must be one after each other. Here again nibe.js is a badly chosen name, a better name would be “FindCheapHoursAdvanced” (or something better).

If you would use the version of the nibe.js which is currently available in #13, you could use the following rule action for your car charger:

dh = require('kolapuuntie/date-helper.js');
nibe = require('kolapuuntie/nibe.js');
influx = require('kolapuuntie/influx.js');

start = dh.getMidnight('start');
stop = dh.getMidnight('stop');

n = 12;
slices = 1;
min = 0;
points = nibe.determineHours(start, stop, n, slices, min);
influx.writePoints('carcharger_control', points);

This would find cheapest 12 individual hours but they do not need to be one after each other.

To continue thinking out loud, you don’t need to necessarily use the midnights as the start and stop. You could for example:

  • create a DateTime Item for when the car needs to be charged.
  • create a Number item for entering the number of hours you need to charge by the time you defined above
  • and then find the user defined number of hours between now and the user defined stop time.

Cheers,
Markus

p.s. I’m currently about to get a 50% discount in my August power bill (just the power, not considering the transfer). I pay 14.8 c/kWh - delta, where delta is how much I’m below the month’s average price if I would use a pure spot priced contract. I’m currently 7.9 c/kWh below the nordpool average so if the month would end now, I would pay 14.8 - 7.9 = 6,9 c/kWh :slight_smile:

I made a major rewrite to the comment marked as solution so that it flows in a more logical order for new readers. No changes to the code was made.

Once again a big thank you, your idea for carcharger control sounds really nice I will try and se if I can make that happend with my limited knowledge. I will really spend some more hours on uvo bluelink api wrapper, there’s some places around the web that covers that, but I haven’t made it yet.

I had a new use case the other day, dishwashers and washingmachines et.c equipment that you can’t pause and resume as often or at all, just some help to figure out a slot “slice” of time for x amount of hours lets say 3hours like our dishwasherprogram needs. We discussed that at home but couldn’t really see if you will have the cheapest hours or hours thta cost 50% of todays maxprice but not run if price is higher than y eur/mWh, so days when electricity is to expensive just power off dishwasher perharps via my zwave network, or integrated apis and stuff.

Good moring

Because SMHI binding shows data as channels and I had to set an item to each channel I have an item for tommorrows average and min temp, now I’m trying to replace your temperature curve with my item.state that holds tomorrows temperature I would have hoped that it schould be as easy as replacing variable t with my item state in Nibe script as below

t = items.getItem(“tomorrowAVGTemperature”).state;

but this throws

07:51:57.288 [INFO ] [enhab.automation.script.ui.16a5a211d5] - nibe.js: Calculating number of ON hours for Nibe...
07:51:57.290 [INFO ] [enhab.automation.script.ui.16a5a211d5] - nibe.js: Number of needed hours: 6.25
07:51:57.290 [INFO ] [enhab.automation.script.ui.16a5a211d5] - nibe.js: Determining on/off hours for Nibe...
07:51:57.292 [ERROR] [ab.automation.script.javascript.stack] - Failed to execute script:
org.graalvm.polyglot.PolyglotException: ReferenceError: "influx" is not defined

I hope someone could point me in the right direction.

@Marcus_Carlsson could you 1) copy-paste the whole rule action and 2) attach the version of you nibe.js that you’re using.

For 1) You should have something like this:

dh = require('kolapuuntie/date-helper.js');
nibe = require('kolapuuntie/nibe.js');
influx = require('kolapuuntie/influx.js');

start = dh.getMidnight('start');
stop = dh.getMidnight('stop');

// You need to replace this with your own average temperature, but that seems
// to work because you were able to get a result that 6.25 hours will be needed.
t = nibe.getForecastTemp(start, stop);

n = nibe.calculateNumberOfHours(t);
slices = 3;
min_heating = 0.1;
points = nibe.determineHours(start, stop, n, slices, min_heating);
influx.writePoints('nibe_control', points);

@masipila

I would agree that it gets some temperature but heres the whole code for rule

configuration: {}
triggers:
  - id: "2"
    configuration:
      cronExpression: 0 0 0 * * ? *
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "1"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: |-
        dh = require('kolapuuntie/date-helper.js');
        nibe = require('kolapuuntie/nibe.js');

        start = dh.getMidnight('start');
        stop = dh.getMidnight('stop');


        t = items.getItem("tomorrowMinTemperature").state;
        n = nibe.calculateNumberOfHours(t);
        nibe.determineHours(start, stop, n, 3, 0.1);
    type: script.ScriptAction

And here’s the nibe.js file
nibe.js.txt (6.6 KB)

Update your script action to be like this:

dh = require('kolapuuntie/date-helper.js');
nibe = require('kolapuuntie/nibe.js');
influx = require('kolapuuntie/influx.js'); // you were missing this

start = dh.getMidnight('start');
stop = dh.getMidnight('stop');

// Get lowest temperature of tomorrow
t = items.getItem("tomorrowMinTemperature").state;

// Determine the number of needed heating hours based on the lowest temperature
n = nibe.calculateNumberOfHours(t);

// Divide tomorrow to this many slices (e.g. 3 x 8 h)
slices = 3;

// Each slice must have 10% of the heating hours
min_heating = 0.1;

// Prepare and save the control points to the database
points = nibe.determineHours(start, stop, n, slices, min_heating);
influx.writePoints('nibe_control', points);

Cheers,
Markus

Hi that’s pretty obvious when you pointed it out. I runs without errore yet, just waiting for tomorrows dayahed prices to se that the amount of hours is equal to what it says in the log, but i will probably be that.

This “nibe” or advanced method is for me a really good job it will be applied in my system in different ways. Now there’s example here for changing temperature source and I think we had an option for not letting it depend on temperature at all, for carcharging. But I still haven’t come to the point of getting my Kias SOC into openHab, my time is running to fast for the moment and alot of stuff to keep up.

Once again thank you.

Hmm

It seem like nibe control doesn’t get any new data I ran the rule after sportprice got updated but there wasn’t more than one hour of on time an the rest isn’t off time for nibe_control somethig is getting strange here.

  1. Did you modify your script action to be exactly the same as I have in the previous comment? There where other differences to what you have in addition to the “require” statement in the beginning.

  2. Increase your log level to DEBUG and copy paste your logs here and I can have a look when I have time

@masipila

This was strange the most crucial part for writing didn’t come along, but now it’s working

nibe.js: Calculating number of ON hours for Nibe...
05:37:14.298 [INFO ] [enhab.automation.script.ui.16a5a211d5] - nibe.js: Number of needed hours: 2.9499999999999993
05:37:14.299 [INFO ] [enhab.automation.script.ui.16a5a211d5] - nibe.js: Determining on/off hours for Nibe...
05:37:14.300 [INFO ] [enhab.automation.script.ui.16a5a211d5] - influx.js: Reading spot prices from the database...
05:37:14.314 [INFO ] [enhab.automation.script.ui.16a5a211d5] - influx.js: Preparing to write points to the database for nibe_control
05:37:14.384 [INFO ] [enhab.automation.script.ui.16a5a211d5] - influx.js: Points successfully saved for measurement nibe_control

One question that still is around my head is that the on state is at 22:13 but I guess your script will cover that or does the control item just starts at 23:00 when toggeling script is running?

Great that you got it working! :+1:

I have observed the same weird offset when looking at the points in the influx data explorer. I asked about it on the InfluxDb community forums but did not get any explanation. When reading them back to openHab, the timestamps are at full hours (at least for me) .

Cheers,
Markus

Yeah really nice and I hope this make electrical bill a little easier to handle during winter. Maybe timestamps is somekind of epoch mathematical issue?

I will have my nibe heatpump run on your Nibe script with the SMHI binding item for average temperature for tomorrow, my zwave smart implant will cut of both electrical boiler in the heatpump and the compressor it self. And I even got the option to run compressor or electrical boiler in their own control due to my heatpump has three aux external control pins that you just close in different combination to achive.

  1. All electricboiler prohibited
  2. Electricboiler prohibited but allow extra hotwater production
  3. Electricalboiler and compressor prohibited

Then I have my balboa spas that I will try to handle in the same metod in the future, just need to figure out how to run your nibe script without temperature dependencies because I just charge up hot water in the tubs as much needed for them not to freeze. Today balboa runs on your waterheaterscript and that works nice but I don’t need several hours in a row as everyhour will heat as much as it does and I will just keep track of temperature.

After that I need to fix the electrical carcharging wich I also think will be the nibescript as foundation but like you said not use midnight because that might be expensive maybe have 18:00-06:00 and find the cheapest 6 hours and just pause and resume the charger (that pause/resume part is allready deployd and in regular use), and maybe have a manual override proxyswitch exposed to googleassistant so you can tell hey google turn on evoverride, kind of. I know the the charger in the car will be messed up if one pause and resume to often it’s like it saying stop nagging me and make up your mind so perharps 2 hours runtime could be good eventhough I think one hourh would be accepted by the car, I just ran into thoose problems when loadbalancing to close to the edge and to fast so it just got up in power and then had to come down in output.But I really like my Easee charger, mainly because of price and all inclusive concept wifi/simcard (would have preferd eth ofcourse but just had one communication outage in a year so that’s okey) and rfid eader, and I could use same RFID tag as in public chargingstations.

Very nice job and all for a good purpose unloading grid primehours and saving money on the fly.