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

Hi

I second that, I just ran my verification in postman and it seems like the api call response is a HTML page.













<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <link rel="icon" type="image/png" href="favicon.png"/>
    <title>ENTSO-E Transparency Platform</title>
    <style type="text/css" media="screen">

        object:focus {
            outline: none;
        }

        /**     * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)     * http://cssreset.com     */
        html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
            margin: 0;
            padding: 0;
            border: 0;
            font-size: 100%;
            font-weight: normal;
            font: inherit;
            vertical-align: baseline;
        }

        /* HTML5 display-role reset for older browsers */
        article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
            display: block;
        }

        body {
            line-height: 1;
        }

        ol, ul {
            list-style: none;
        }

        blockquote, q {
            quotes: none;
        }

        blockquote:before, blockquote:after, q:before, q:after {
            content: none;
        }

        table {
            border-collapse: collapse;
            border-spacing: 0;
        }

        input, select, textarea {
            outline: none;
        }

        /* UOP core style sheets */
        html {
            text-align: center;
            font-family: Arial, sans-serif;
        }

        body {
            text-align: left;
            margin-left: auto;
            margin-right: auto;
            width: 1024px;


As both my heatpump and waterheater on timing is depending on the spot data from entso-e I had to make a manual backup for situations like this. I did a rule that I edit according to spot prices from other sources that sets the control points in the Influx database…


influx = require('/etc/openhab/scripts/kotikolo/influx.js');

points = [
  { "datetime": "2023-02-14T22:00:00Z", "value": 0 },
  { "datetime": "2023-02-14T23:00:00Z", "value": 1 },
  { "datetime": "2023-02-15T00:00:00Z", "value": 1 },
  { "datetime": "2023-02-15T01:00:00Z", "value": 1 },
  { "datetime": "2023-02-15T02:00:00Z", "value": 1 },
  { "datetime": "2023-02-15T03:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T04:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T05:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T06:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T07:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T08:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T09:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T10:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T11:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T12:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T13:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T14:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T15:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T16:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T17:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T18:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T19:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T20:00:00Z", "value": 0 },
  { "datetime": "2023-02-15T21:00:00Z", "value": 0 }
]

influx.writePoints('heatpump_control', points); 

A couple of failsafe considerations.

  1. I run the “fetch spot prices” several times per day (first time 14.30 EET, then 15.30 EET, …) so that the likelihood for not having the prices would decrease. I managed to get the spot prices for tomorrow before Entso-E went down.

  2. The hourly scripts that read the current control point is designed so that if a control value is not found, value 1 is defaulted as a failsafe.

  3. I have implemented this kind of an UI for manually adjusting (or creating) the control points:

Translation for non-Finnish speakers:

  • The first item is “number of heating hours”. This one is automatically set based on the weather forecast but I can manually adjust this. I have a rule that whenever the value of this Item changes, the control points are re-calculated.
  • The second item is “number of heating slices”. The control points are re-calculated also if the value of this Item changes.
  • Third one is a datetime Item where I can define a full hour that I want to force ON. There is a rule that whenever the value is changed, value 1 control point is written for the selected hour.
  • Fourth one is a datetime Item where I can define a full hour that I want to force OFF. There is a rule that whenever the value is changed, value 0 control point is written for the selected hour.

(This “control parameters” is a tabbed layout and I have a separate tab for house heating, hot water heating and car charging, but they are conceptually all same)

Cheers,
Markus

1 Like

I got that error at 14:30 FST, too. At 15:30 FST the API response was OK.

I am no coder, and OpenHAB graphs are a stretch for me. With @masipila’s code I was able to get a nice graph like this.

I control the Nibe 1245 ground source heat pump with a Tasmota Wifi relay. The same relay board has a couple of GPIO pins that can be used to read the pump status from Nibe 1245 aux AA3-X7. This way I can monitor both the control and the actual performance of the pump like this:

I added one more section to @masipila’s code to read Nibe1245Sensor from InfluxDB, I think, like so:

    - component: oh-time-series
      config:
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: nibe_control
        yAxisIndex: 2
        name: Nibe 1245 ohjaus
        service: influxdb
    - component: oh-time-series
      config:
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: Nibe1245Sensor
        yAxisIndex: 2
        name: Nibe 1245 toteuma
        service: influxdb

This does not work, as the graph looks now looks like this:

How to fix this?

Timo

Did you notice this:

That was a good spoting I did manage to repalce url to new endpoint (https://web-api.tp.entsoe.eu/api?securityToken=)and in postman get an aswer looking like this

<?xml version="1.0" encoding="UTF-8"?>
<Publication_MarketDocument xmlns="urn:iec62325.351:tc57wg16:451-3:publicationdocument:7:0">
    <mRID>6fd1d74f825e46fba88c59126154a7ed</mRID>
    <revisionNumber>1</revisionNumber>
    <type>A44</type>
    <sender_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</sender_MarketParticipant.mRID>
    <sender_MarketParticipant.marketRole.type>A32</sender_MarketParticipant.marketRole.type>
    <receiver_MarketParticipant.mRID codingScheme="A01">10X1001A1001A450</receiver_MarketParticipant.mRID>
    <receiver_MarketParticipant.marketRole.type>A33</receiver_MarketParticipant.marketRole.type>
    <createdDateTime>2023-02-15T15:48:14Z</createdDateTime>
    <period.timeInterval>
        <start>2023-02-13T23:00Z</start>
        <end>2023-02-15T23:00Z</end>
    </period.timeInterval>
    <TimeSeries>
        <mRID>1</mRID>
        <businessType>A62</businessType>
        <in_Domain.mRID codingScheme="A01">10Y1001A1001A46L</in_Domain.mRID>
        <out_Domain.mRID codingScheme="A01">10Y1001A1001A46L</out_Domain.mRID>
        <currency_Unit.name>EUR</currency_Unit.name>
        <price_Measure_Unit.name>MWH</price_Measure_Unit.name>
        <curveType>A01</curveType>
        <Period>
            <timeInterval>
                <start>2023-02-13T23:00Z</start>
                <end>2023-02-14T23:00Z</end>
            </timeInterval>
            <resolution>PT60M</resolution>
            <Point>
                <position>1</position>
                <price.amount>38.21</price.amount>
            </Point>
            <Point>
                <position>2</position>
                <price.amount>55.69</price.amount>
            </Point>
            <Point>
                <position>3</position>
                <price.amount>85.06</price.amount>
            </Point>
            <Point>
                <position>4</position>
                <price.amount>104.94</price.amount>
            </Point>
            <Point>
                <position>5</position>
                <price.amount>106.33</price.amount>
            </Point>
            <Point>
                <position>6</position>
                <price.amount>108.27</price.amount>
            </Point>
            <Point>
                <position>7</position>
                <price.amount>120.64</price.amount>
            </Point>
            <Point>
                <position>8</position>
                <price.amount>193.79</price.amount>
            </Point>
            <Point>
                <position>9</position>
                <price.amount>200.57</price.amount>
            </Point>
            <Point>
                <position>10</position>
                <price.amount>174.61</price.amount>
            </Point>
            <Point>
                <position>11</position>
                <price.amount>121.67</price.amount>
            </Point>
            <Point>
                <position>12</position>
                <price.amount>121.18</price.amount>
            </Point>
            <Point>
                <position>13</position>
                <price.amount>125.84</price.amount>
            </Point>
            <Point>
                <position>14</position>
                <price.amount>119.04</price.amount>
            </Point>
            <Point>
                <position>15</position>
                <price.amount>122.01</price.amount>
            </Point>
            <Point>
                <position>16</position>
                <price.amount>117.55</price.amount>
            </Point>
            <Point>
                <position>17</position>
                <price.amount>142.08</price.amount>
            </Point>
            <Point>
                <position>18</position>
                <price.amount>176.47</price.amount>
            </Point>
            <Point>
                <position>19</position>
                <price.amount>193.59</price.amount>
            </Point>
            <Point>
                <position>20</position>
                <price.amount>129.72</price.amount>
            </Point>
            <Point>
                <position>21</position>
                <price.amount>109.36</price.amount>
            </Point>
            <Point>
                <position>22</position>
                <price.amount>109.03</price.amount>
            </Point>
            <Point>
                <position>23</position>
                <price.amount>106.06</price.amount>
            </Point>
            <Point>
                <position>24</position>
                <price.amount>104.11</price.amount>
            </Point>
        </Period>
    </TimeSeries>
    <TimeSeries>
        <mRID>2</mRID>
        <businessType>A62</businessType>
        <in_Domain.mRID codingScheme="A01">10Y1001A1001A46L</in_Domain.mRID>
        <out_Domain.mRID codingScheme="A01">10Y1001A1001A46L</out_Domain.mRID>
        <currency_Unit.name>EUR</currency_Unit.name>
        <price_Measure_Unit.name>MWH</price_Measure_Unit.name>
        <curveType>A01</curveType>
        <Period>
            <timeInterval>
                <start>2023-02-14T23:00Z</start>
                <end>2023-02-15T23:00Z</end>
            </timeInterval>
            <resolution>PT60M</resolution>
            <Point>
                <position>1</position>
                <price.amount>72.17</price.amount>
            </Point>
            <Point>
                <position>2</position>
                <price.amount>69.29</price.amount>
            </Point>
            <Point>
                <position>3</position>
                <price.amount>66.94</price.amount>
            </Point>
            <Point>
                <position>4</position>
                <price.amount>67.78</price.amount>
            </Point>
            <Point>
                <position>5</position>
                <price.amount>72.17</price.amount>
            </Point>
            <Point>
                <position>6</position>
                <price.amount>69.29</price.amount>
            </Point>
            <Point>
                <position>7</position>
                <price.amount>109.05</price.amount>
            </Point>
            <Point>
                <position>8</position>
                <price.amount>112.97</price.amount>
            </Point>
            <Point>
                <position>9</position>
                <price.amount>182.72</price.amount>
            </Point>
            <Point>
                <position>10</position>
                <price.amount>150.98</price.amount>
            </Point>
            <Point>
                <position>11</position>
                <price.amount>128.67</price.amount>
            </Point>
            <Point>
                <position>12</position>
                <price.amount>114.74</price.amount>
            </Point>
            <Point>
                <position>13</position>
                <price.amount>113.52</price.amount>
            </Point>
            <Point>
                <position>14</position>
                <price.amount>112.87</price.amount>
            </Point>
            <Point>
                <position>15</position>
                <price.amount>112.83</price.amount>
            </Point>
            <Point>
                <position>16</position>
                <price.amount>112.75</price.amount>
            </Point>
            <Point>
                <position>17</position>
                <price.amount>112.88</price.amount>
            </Point>
            <Point>
                <position>18</position>
                <price.amount>112.18</price.amount>
            </Point>
            <Point>
                <position>19</position>
                <price.amount>108.92</price.amount>
            </Point>
            <Point>
                <position>20</position>
                <price.amount>104.96</price.amount>
            </Point>
            <Point>
                <position>21</position>
                <price.amount>73.74</price.amount>
            </Point>
            <Point>
                <position>22</position>
                <price.amount>66.17</price.amount>
            </Point>
            <Point>
                <position>23</position>
                <price.amount>58.34</price.amount>
            </Point>
            <Point>
                <position>24</position>
                <price.amount>49.59</price.amount>
            </Point>
        </Period>
    </TimeSeries>
</Publication_MarketDocument>

However openhab entsoe.js cannot parse that

entsoe.js: Exception parsing spot prices: Cannot read property “period.timeInterval” from undefined

But this could have been some seession trouble from postman session, after a few refresh it worked boot in postman and openhab.

You could try to change url in entsoe.js dunction makeApiCall row 51 for me.

1 Like

In my entsoe.js it is row 68 that has the url to change. With this change everything works again:

    const url =
          'https://web-api.tp.entsoe.eu/api?' +

1 Like

Thanks @timo12357 ! This is what open source communities are at their best! I was doing sports with my younger kid and by the time I got home the community had already solved the issue <3

I updated the URL to the entsoe.js.txt which is attached to comment #13.

Thanks!

Markus

1 Like

Yeah I forgot to mention that I’m running an early generation oc scripts. I didn’t update when newer arrived. But it sure did the trick.

The difference being that you leave decision on action to compressor to the pump’s control.
It’s a really widespread misbelief that people think they can do control better than the pump vendor can. I don’t believe it, and even those who do usually that’s turning 180 degress once you had to discuss with your vendor who is to pay for a repair.
It’s ok to do it in your solution but if your intention is to share and apply your code to other houses and pumps, too, you should not nudge users into that situation, and should use a standard interface logic instead that apply to all pumps likewise. That’s what only SGr can be.
I suggest to have a look if you can use modbus or other control to block your compressor for DHW
(domestic hot water).

so “1” does not refer to the number of slices but the maximum slice size ?

Ok then, but as that would always get you the cheapest power, why would there ever be a reason to use anything slice = larger than 1 ?

I’m not forcing anyone to do anything with their pumps which they don’t fully understand.

Quoting myself from the top of #13:

and

Coming back to the actual discussion here. I tend to agree with you that it would be good to update the “solution” so that it would use the SG Ready modes as the first method and then it could be mentioned that there’s also these other more intrusive external control options if somebody wants to go that path and understand what they are doing.

In general it would probably be a good idea to split this whole thing to separate threads so that there would be one page for the introduction, another for setting up the things, etc because this thread is now starting to be overwhelmingly long… I remember reading through some series by Rich Koshak which was written in that format where each “part” was then linking to a thread of its own… (not tagging him here as there’s no need to spam him with this thinking-out-loud thing…)

  • The slice is not the same thing as an individual heating hour.
  • A typical optimization period is the day ahead (in the context of spot price price optimization), i.e. 24 hours from midnight to midnight.
  • The point of the “slicing” is to address days where the spot price distribution looks like this:

On this example day 9 hours of heating was allowed.

  • The hours starting at 21, 22 and 23 are cheaper than the hours starting at 13, 14 and 15.
  • But it made more sense to pick the hours of 13, 14 and 15 for heating in favor of 21, 22 and 23 because otherwise the house would have been cooling from 06:00 until 21:00, which would have been too long on that temperature.
  • If we have 3 slices, it means that we divide the 24h period into 3x8 h periods and we can guarantee that there will be configured amount of heating within all those 8h periods.
  • (In this particular example the slicing algorithm originally picked / forced some heating hours for the hours 21, 22 and 23 but I had manually removed those from my UI because the hours starting from 00 were cheaper than 21, 22, 23)
1 Like

My ground source heat pump is 10 years old.There is no warranty left, I pay all reparations on this device in any fore- and unforeseeable case. Reducing starts per day is a key target for me to extend its lifetime beyond its planned or unplanned obsolecense. @masipila’s solution enables me to reduce starts per day, while also selecting the cheapest hours while doing it. This is good enough for me.

@mstormi Additional thinking-out-loud in the generalization of this solution for wider usage in addition to recommend the SG Ready modes first.

One of first hurdles over here is the fact that persisting the day-ahead spot prices currently requires me to bypass openHab persistence layer. I would like to contribute in making a proper Entso-E day-ahead binding but I’ve learnt that bindings cannot persist data and the definitely cannot persist a future time series.

If you have time and thoughts on these, I would appreciate your thoughts but these places are better threads for those discussions than this one…

See

Cheers,
Markus

Entso-E is again having troubles on publishing the prices for tomorrow.

Here is a small javascript that writes the Finnish prices (10% VAT) for tomorrow:

influx = require('kolapuuntie/influx.js');
measurement = 'spot_price';
points = [
  { "datetime":"2023-02-16T23:00:00Z", "value": 3.85 },
  { "datetime":"2023-02-17T00:00:00Z", "value": 3.47 },
  { "datetime":"2023-02-17T01:00:00Z", "value": 3.54 },
  { "datetime":"2023-02-17T02:00:00Z", "value": 3.53 },
  { "datetime":"2023-02-17T03:00:00Z", "value": 3.81 },
  { "datetime":"2023-02-17T04:00:00Z", "value": 5.8 },
  { "datetime":"2023-02-17T05:00:00Z", "value": 8.62 },
  { "datetime":"2023-02-17T06:00:00Z", "value": 10.34 },
  { "datetime":"2023-02-17T07:00:00Z", "value": 10.34 },
  { "datetime":"2023-02-17T08:00:00Z", "value": 9.79 },
  { "datetime":"2023-02-17T09:00:00Z", "value": 9.62 },
  { "datetime":"2023-02-17T10:00:00Z", "value": 10.17 },
  { "datetime":"2023-02-17T11:00:00Z", "value": 10.81 },
  { "datetime":"2023-02-17T12:00:00Z", "value": 9.89 },
  { "datetime":"2023-02-17T13:00:00Z", "value": 7.71 },
  { "datetime":"2023-02-17T14:00:00Z", "value": 6.59 },
  { "datetime":"2023-02-17T15:00:00Z", "value": 6.42 },
  { "datetime":"2023-02-17T16:00:00Z", "value": 5.83 },
  { "datetime":"2023-02-17T17:00:00Z", "value": 4.4 },
  { "datetime":"2023-02-17T18:00:00Z", "value": 4.05 },
  { "datetime":"2023-02-17T19:00:00Z", "value": 3.81 },
  { "datetime":"2023-02-17T20:00:00Z", "value": 3.62 },
  { "datetime":"2023-02-17T21:00:00Z", "value": 3.28 }
]
influx.writePoints(measurement, points);

You must have some typo in the YAML syntax. It’s much easier to configure the graph with the Design UI

Select the “configure series” and select the Item which you want to add.

To make this Bar Chart Item to be “stacked” with the other Bar Chart Items, you need to add the “stack” attribute to the YAML code because the openHab Design UI does not have this feature of Apache ECharts:
image

Hmm ok, but from a 30000ft view, you’re now optimizing in two dimensions, cost and comfort these are.
There’s no ‘objective’ single optimum in those, it depends how you weigh them in comparison and everyone’s mileage will vary. If for example you asked me, it would be 100% on cost savings, but if you asked my wife it sure would have a bias towards ‘have it warm at all times’.

Then again, my claim on this is that any house has sufficient thermal buffer capacity to compensate those dips. Plus almost any heat pump user has a water buffer so you need not produce heat the instant you need it to heat, you have a thermal battery that allows to postpone heating to the cheapest hours.
You effectively will not notice the dip. Well unless you have a non-isolated hut north of the polar circle, but then you likely won’t be operating a heat pump in any such ‘home’.
I’m a big fan of the KISS principle. So rather than to complicate calculations and code for it and nonetheless end up with an optimization that will be well below 100% (and very depending on who you ask), going for an all-financial optimization will get you those 100% without that you have to pay a relevant price for the comfort you lose. I’d claim it’s negligible.
(just don’t tell anyone - as soon as he or she knows, he or she WILL notice a “significant” drop in temperature due to well known psychological effects, just like you start shivering the moment you make yourself aware that winter is coming).

As you requested I responded over there in the other thread. I don’t see the need to bypass persistence but I understand that once your existing code has a focus on that idea it can be hard to back off from it. Requires substantial out-of-the-box thinking at times.

Glad to hear it does the job for you.

Modern, properly configured heat pumps however don’t start often any more, particularly as they can modulate rather than to repeatedly toggle on/off.
So I’d dare to postulate that for most people, this isn’t a good key target as it will not substantially increase longevity but it’ll cost you some (financial) efficiency.

That is very true. Modern ground source heatpumps use a variable frequency drive to control the compressor motor. These ground source heat pumps are designed to never stop or start, just to change the motor speed down and back up when needed. As far as I understand this extends the expected lifetime of the compressor significantly. Stopping the motor of such a system does not make any sense, as it would add starts and stops thereby adding risk of compressor failure. For ground source heat pumps that have a variable frequency drive SGready is the way to go.

Nibe 1226 and Nibe 1245 are from a time before variable frequency drives had made their way into ground source heat pumps. For an old ground source heat pump like these, it makes sense to try to force the pump to run in one go for as long as needed and to start it as few times as possible per day.

Why does the algorithm select the hour at 19:00 rather than the cheaper hour at 20:00?

2 slices, 8 allowed hours, midnight to midnight, 40% minimum share.

Timo