node-red-contrib-power-saver 3.6.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +15 -0
- package/docs/.vuepress/components/DonateButtons.vue +26 -3
- package/docs/.vuepress/components/VippsPlakat.vue +20 -0
- package/docs/.vuepress/config.js +18 -10
- package/docs/.vuepress/public/ads.txt +1 -0
- package/docs/README.md +4 -4
- package/docs/changelog/README.md +59 -1
- package/docs/contribute/README.md +8 -3
- package/docs/examples/README.md +2 -0
- package/docs/examples/example-grid-tariff-capacity-flow.json +1142 -0
- package/docs/examples/example-grid-tariff-capacity-part.md +988 -107
- package/docs/faq/README.md +1 -1
- package/docs/faq/best-save-viewer.md +1 -1
- package/docs/guide/README.md +20 -5
- package/docs/images/best-save-config.png +0 -0
- package/docs/images/combine-two-lowest-price.png +0 -0
- package/docs/images/example-capacity-flow.png +0 -0
- package/docs/images/fixed-schedule-config.png +0 -0
- package/docs/images/global-context-window.png +0 -0
- package/docs/images/lowest-price-config.png +0 -0
- package/docs/images/node-ps-schedule-merger.png +0 -0
- package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
- package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
- package/docs/images/schedule-merger-config.png +0 -0
- package/docs/images/schedule-merger-example-1.png +0 -0
- package/docs/images/vipps-plakat.png +0 -0
- package/docs/images/vipps-qr.png +0 -0
- package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
- package/docs/nodes/README.md +12 -6
- package/docs/nodes/dynamic-commands.md +79 -0
- package/docs/nodes/dynamic-config.md +76 -0
- package/docs/nodes/ps-elvia-add-tariff.md +4 -0
- package/docs/nodes/ps-general-add-tariff.md +10 -0
- package/docs/nodes/ps-receive-price.md +2 -1
- package/docs/nodes/ps-schedule-merger.md +227 -0
- package/docs/nodes/ps-strategy-best-save.md +46 -110
- package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
- package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
- package/docs/nodes/ps-strategy-lowest-price.md +51 -112
- package/package.json +5 -2
- package/src/elvia/elvia-add-tariff.html +1 -2
- package/src/elvia/elvia-add-tariff.js +1 -3
- package/src/elvia/elvia-api.js +9 -0
- package/src/elvia/elvia-tariff.html +1 -1
- package/src/general-add-tariff.html +14 -8
- package/src/general-add-tariff.js +0 -1
- package/src/handle-input.js +94 -106
- package/src/handle-output.js +109 -0
- package/src/receive-price-functions.js +3 -3
- package/src/schedule-merger-functions.js +98 -0
- package/src/schedule-merger.html +135 -0
- package/src/schedule-merger.js +108 -0
- package/src/strategy-best-save.html +38 -1
- package/src/strategy-best-save.js +17 -63
- package/src/strategy-fixed-schedule.html +339 -0
- package/src/strategy-fixed-schedule.js +84 -0
- package/src/strategy-functions.js +35 -0
- package/src/strategy-lowest-price.html +76 -38
- package/src/strategy-lowest-price.js +16 -35
- package/src/utils.js +75 -2
- package/test/commands-input-best-save.test.js +142 -0
- package/test/commands-input-lowest-price.test.js +149 -0
- package/test/commands-input-schedule-merger.test.js +128 -0
- package/test/data/best-save-overlap-result.json +5 -1
- package/test/data/best-save-result.json +4 -0
- package/test/data/commands-result-best-save.json +383 -0
- package/test/data/commands-result-lowest-price.json +340 -0
- package/test/data/fixed-schedule-result.json +353 -0
- package/test/data/lowest-price-result-cont-max-fail.json +5 -1
- package/test/data/lowest-price-result-cont-max.json +3 -1
- package/test/data/lowest-price-result-cont.json +8 -1
- package/test/data/lowest-price-result-missing-end.json +8 -3
- package/test/data/lowest-price-result-neg-cont.json +27 -0
- package/test/data/lowest-price-result-neg-split.json +23 -0
- package/test/data/lowest-price-result-split-allday.json +3 -1
- package/test/data/lowest-price-result-split-allday10.json +1 -0
- package/test/data/lowest-price-result-split-max.json +3 -1
- package/test/data/lowest-price-result-split.json +3 -1
- package/test/data/merge-schedule-data.js +238 -0
- package/test/data/negative-prices.json +197 -0
- package/test/data/nordpool-event-prices.json +96 -480
- package/test/data/nordpool-zero-prices.json +90 -0
- package/test/data/reconfigResult.js +1 -0
- package/test/data/result.js +1 -0
- package/test/data/tibber-result-end-0-24h.json +12 -2
- package/test/data/tibber-result-end-0.json +12 -2
- package/test/data/tibber-result.json +1 -0
- package/test/receive-price.test.js +22 -0
- package/test/schedule-merger-functions.test.js +101 -0
- package/test/schedule-merger-test-utils.js +27 -0
- package/test/schedule-merger.test.js +130 -0
- package/test/send-config-input.test.js +45 -2
- package/test/strategy-best-save-test-utils.js +1 -1
- package/test/strategy-best-save.test.js +45 -0
- package/test/strategy-fixed-schedule.test.js +117 -0
- package/test/strategy-heat-capacitor.test.js +1 -1
- package/test/strategy-lowest-price-functions.test.js +1 -1
- package/test/strategy-lowest-price-test-utils.js +31 -0
- package/test/strategy-lowest-price.test.js +55 -45
- package/test/test-utils.js +43 -36
- package/test/utils.test.js +13 -0
- package/docs/images/node-power-saver.png +0 -0
- package/docs/nodes/power-saver.md +0 -23
- package/src/power-saver.html +0 -116
- package/src/power-saver.js +0 -260
- package/test/commands-input.test.js +0 -47
- package/test/power-saver.test.js +0 -189
|
@@ -1,69 +1,253 @@
|
|
|
1
1
|
# Capacity part of grid tariff
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
The fee is calculated based on the average consumption per hour. The maximum average consumption for each day is calculated. Then the average of the three highest days for the month is used to decide your capacity fee.
|
|
3
|
+
::: danger Bug-fix 12. September 2022
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
::: details A bug was found 12. sep 2022. Here is how to fix:
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
### 1. Node "Find highest per day":
|
|
8
|
+
|
|
9
|
+
Replace this line:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const highestToday = days.get(new Date().getDate());
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With this code:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const highestToday = days.get(new Date().getDate()) ?? {
|
|
19
|
+
consumption: 0,
|
|
20
|
+
from: null,
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will set the `highestToday` to 0 during the first hour.
|
|
25
|
+
|
|
26
|
+
### 2. Node "Calculate values":
|
|
27
|
+
|
|
28
|
+
Above this line:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
function calculateLevel(hourEstimate, ...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Insert this code:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
function isNull(value) {
|
|
38
|
+
return value === null || value === undefined;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Further down the code you can find these 3 lines with 4 lines between:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
if (!highestPerDay) {
|
|
46
|
+
if (!highestToday) {
|
|
47
|
+
if (!hourEstimate) {
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Change these to:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
if (isNull(highestPerDay)) {
|
|
54
|
+
if (isNull(highestToday)) {
|
|
55
|
+
if (isNull(hourEstimate)) {
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then these will not fail the first hour.
|
|
59
|
+
|
|
60
|
+
### 3. Node "Build query for consumption":
|
|
61
|
+
|
|
62
|
+
Find this line:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const hour = time.getMinutes(); // NB Change to getMinutes()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Change it to:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const hour = time.getHours();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The bug fixed on no. 3 does so data for hours are read every minute,
|
|
75
|
+
instead of every hour. This is not necessary.
|
|
76
|
+
However, it does not lead to any error.
|
|
77
|
+
:::
|
|
78
|
+
|
|
79
|
+
## Introduction
|
|
80
|
+
|
|
81
|
+
I Norway, there has been introduced a monthly fee for grid capacity.
|
|
82
|
+
The purpose is to get people to avoid peaks of high capacity usage.
|
|
83
|
+
|
|
84
|
+
The fee is calculated based on the average consumption per hour (kWh).
|
|
85
|
+
For each day, the worst hour is used, that is the hour with highest consumption that day.
|
|
86
|
+
Then the 3 days where this number is highest (the 3 highest) is used.
|
|
87
|
+
The average of these 3 days is calculated, and the result decides what capacity step you will pay for.
|
|
88
|
+
|
|
89
|
+
The steps are for example like this:
|
|
90
|
+
|
|
91
|
+
- 0-2 kW
|
|
92
|
+
- 2-5 kW
|
|
93
|
+
- 5-10 kW
|
|
94
|
+
- 10-15 kW
|
|
95
|
+
- 15-20 kW
|
|
96
|
+
|
|
97
|
+
This may be different for different grid providers.
|
|
98
|
+
|
|
99
|
+
If, for example, the average of the 3 worst days is 6 kW, you pay for step 5-10 kW.
|
|
100
|
+
|
|
101
|
+
Here is an example how this can be controlled. The example contains several features,
|
|
102
|
+
and you may not want to use them all, so it is a good idea to read through it all before you decide how to use it.
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
|
|
106
|
+
The first part of nodes (upper left) is used to read consumption from Tibber and to perform all calculations.
|
|
107
|
+
The second part (right) is used to update a set of sensors in HA. You can use those sensor for many
|
|
108
|
+
purposes related to the grid capacity.
|
|
109
|
+
The third part (bottom left) is used to take actions in order to reduce power consumption,
|
|
110
|
+
or to reset actions when the consumption is lowered.
|
|
111
|
+
|
|
112
|
+
You may use the whole example, or only part of it, so you should read through the whole documentation before you start,
|
|
113
|
+
so you can decide what to use and how to use it.
|
|
114
|
+
|
|
115
|
+
The complex parts are solved by scripts in the function nodes, and you will have to change parts of them to use it all.
|
|
116
|
+
|
|
117
|
+
::: warning No guarantee
|
|
118
|
+
There is no guarantee that this works, so use at your own risk.
|
|
119
|
+
:::
|
|
9
120
|
|
|
10
121
|
## Requirements
|
|
11
122
|
|
|
12
123
|
You need the following to be able to use this example:
|
|
13
124
|
|
|
125
|
+
- Home Assistant with Node-RED
|
|
14
126
|
- Tibber Pulse to measure consumption continuously.
|
|
15
127
|
- Tibber subscription to get consumption data per hour.
|
|
16
128
|
- Token to access Tibber API.
|
|
17
129
|
- [Node-RED Companion Integration](https://github.com/zachowj/hass-node-red)
|
|
18
|
-
- Tibber nodes in Node-RED.
|
|
130
|
+
- Tibber nodes in Node-RED [(node-red-contrib-tibber-api)](https://flows.nodered.org/node/node-red-contrib-tibber-api).
|
|
19
131
|
|
|
20
132
|
If you have the same information from other sources, you may be able to adapt the example.
|
|
21
133
|
|
|
22
134
|
## Features
|
|
23
135
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
136
|
+
- 15-20 sensors in HA showing information related to the capacity and the calculation, including:
|
|
137
|
+
- Status Ok, Warning or Alarm related to current consumption compared to the next step.
|
|
138
|
+
- Alarm level 0-9 based on how serious the current consumption is.
|
|
139
|
+
- Current step based on usage the whole month.
|
|
140
|
+
- The highest consumption any hour today.
|
|
141
|
+
- Reduction required to avoid breaking the limit.
|
|
142
|
+
- Reduction recommended to reduce risk of breaking he limit.
|
|
143
|
+
- Estimates for the current hour.
|
|
144
|
+
- Current consumption based on average the last minute (can be configured).
|
|
145
|
+
- Automatically take actions to reduce consumption if it is recommended or required.
|
|
146
|
+
- Automatically reset actions if the consumption is sufficiently reduced.
|
|
147
|
+
- Log actions taken to a file
|
|
27
148
|
|
|
28
149
|
## Algorithm
|
|
29
150
|
|
|
30
|
-
The
|
|
151
|
+
The consumption is measured by Tibber Pulse continuously (every 2 seconds),
|
|
152
|
+
and is used to calculate average consumption the last minute.
|
|
153
|
+
(Can be configured to another number of minutes.)
|
|
154
|
+
**It is assumed that you will have this consumption the rest of the current hour.**
|
|
155
|
+
Based on the real consumption until now this hour, and the assumed consumption the
|
|
156
|
+
rest of the hour, the total for the current hour is estimated. This is the `hourEstimate`.
|
|
31
157
|
|
|
32
|
-
|
|
33
|
-
- Warning - Current estimate may affect the step, if combined with high consumption later this month.
|
|
34
|
-
- Alarm - Current estimate will bring you over to another step.
|
|
158
|
+
The highest hour previously this day is found as `highestToday`.
|
|
35
159
|
|
|
36
|
-
The
|
|
37
|
-
|
|
160
|
+
The highest hour for each day this month is found, and the 3 highest is used
|
|
161
|
+
in the calculation as the `highestCounting`.
|
|
38
162
|
|
|
39
|
-
Then the
|
|
163
|
+
Then the `hourEstimate` is compared to those other numbers to calculate an alarm level.
|
|
40
164
|
|
|
41
|
-
If the current hour is todays worst, it is ranked together with the other days this month.
|
|
165
|
+
If the current hour is todays worst, it is ranked together with the other days this month.
|
|
166
|
+
If today is ranked as 4 or better, where 1 is the worst this month, then status is Ok.
|
|
167
|
+
It will not affect the fee.
|
|
42
168
|
|
|
43
169
|
The `currentStep` is decided based on the 3 worst days, including today, but not considering the current hour.
|
|
44
170
|
|
|
45
|
-
If today is ranked as 1, 2 or 3, it may affect the fee. Then the average of the three worst days, including this hour, is calculated. This is `currentMonthlyEstimate`.
|
|
171
|
+
If today is ranked as 1, 2 or 3, it may affect the fee. Then the average of the three worst days, including this hour, is calculated. This is `currentMonthlyEstimate`.
|
|
172
|
+
|
|
173
|
+
Other sensor values are calculated based on values available.
|
|
174
|
+
See description for each of them for details.
|
|
175
|
+
|
|
176
|
+
## Calculated sensor values
|
|
177
|
+
|
|
178
|
+
The `Calculate values` node creates a payload with values that are used to create
|
|
179
|
+
sensors in Home Assistant. There is one `entity` node for each sensor value.
|
|
180
|
+
You may delete the entity nodes for sensor values you don't need.
|
|
181
|
+
|
|
182
|
+
Here is a description of each of those sensor values.
|
|
183
|
+
|
|
184
|
+
### Status
|
|
185
|
+
|
|
186
|
+
The status of the current hour as text: `Ok`, `Warning` or `Alarm`.
|
|
187
|
+
|
|
188
|
+
`Alarm` means that if you do not reduce the consumption, you will break the limit of the next step.
|
|
189
|
+
|
|
190
|
+
`Warning` means that there is an increased risk for breaking the limit.
|
|
191
|
+
|
|
192
|
+
`Ok` means you are pretty safe.
|
|
193
|
+
|
|
194
|
+
### Ok
|
|
195
|
+
|
|
196
|
+
A boolean value, `true` if the status is `Ok`, `false` if not.
|
|
197
|
+
|
|
198
|
+
### Warning
|
|
199
|
+
|
|
200
|
+
A boolean value, `true` if the status is `Warning`, `false` if not.
|
|
201
|
+
|
|
202
|
+
### Alarm
|
|
46
203
|
|
|
47
|
-
|
|
204
|
+
A boolean value, `true` if the status is `Alarm`, `false` if not.
|
|
48
205
|
|
|
49
|
-
|
|
206
|
+
You can configure the constant `ALARM` to the lowest level that will cause status `Alarm`. Default is `8`,
|
|
207
|
+
meaning you have a small buffer (the `BUFFER`).
|
|
50
208
|
|
|
51
|
-
|
|
209
|
+
### Alarm Level
|
|
52
210
|
|
|
53
|
-
|
|
211
|
+
A number from 0 to 9, meaning as follows:
|
|
54
212
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
213
|
+
| Level | Description |
|
|
214
|
+
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
215
|
+
| 0 | You're good, either because consumption is low or because there was a worse hour earlier today. |
|
|
216
|
+
| 9 | The current consumption will lead to breaking the limit for the next step, if continued. |
|
|
217
|
+
| 8 | Same as `9`but with a buffer you can configure to reduce risk. The buffer is a constant named `BUFFER`, and the default value is 0.5 kWh |
|
|
218
|
+
| 7 | The `hourEstimate` (estimate for the hour) is higher than the limit for the next step, but since previous consumption has been low, the average of the 3 worst is below the limit, so you will not break the limit **yet (!)**. |
|
|
219
|
+
| 6 | Same as 7 but with a the same buffer as for alarm level `8` |
|
|
220
|
+
| 5 | The `hourEstimate` is the worst this month, and it is only a _safe zone_ away from breaking the limit. The safe zone can be configured using the constant called `SAFE_ZONE`, default 2 kWh. |
|
|
221
|
+
| 4 | The `hourEstimate` is the second to worst this month, and it is only a _safe zone_ away from breaking the limit. |
|
|
222
|
+
| 3 | The `hourEstimate` is the third to worst this month, and it is only a _safe zone_ away from breaking the limit. |
|
|
223
|
+
| 2 | The `hourEstimate` is the worst this month. Consumption may still be low. |
|
|
224
|
+
| 1 | The `hourEstimate` is the second to worst this month. Consumption may still be low. |
|
|
65
225
|
|
|
66
|
-
###
|
|
226
|
+
### Current Step
|
|
227
|
+
|
|
228
|
+
Shows the step you are currently on (under), that is the limit you do not want to break.
|
|
229
|
+
The steps are configured using the constant `STEPS` in the script.
|
|
230
|
+
The default is
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
const STEPS = [2, 5, 10, 15, 20]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
::: warning Configure steps
|
|
237
|
+
You must edit the `STEPS`constant in the script.
|
|
238
|
+
Set it to the relevant steps for your grid supplier.
|
|
239
|
+
You should omit steps that you do not plan to stay under,
|
|
240
|
+
so you avoid alarms and actions for that step in the beginning of the month.
|
|
241
|
+
:::
|
|
242
|
+
|
|
243
|
+
### Hour Estimate
|
|
244
|
+
|
|
245
|
+
The estimated consumption for the current hour (`hourEstimate`).
|
|
246
|
+
This is calculated as real consumption earlier this hour plus estimated consumption for the rest of the hour.
|
|
247
|
+
|
|
248
|
+
### Current Hour Ranking
|
|
249
|
+
|
|
250
|
+
A number (0-4) ranking the current hour with the 3 highest hours this month.
|
|
67
251
|
|
|
68
252
|
The current hour ranking has the following meaning:
|
|
69
253
|
|
|
@@ -75,43 +259,70 @@ The current hour ranking has the following meaning:
|
|
|
75
259
|
| 3 | This is estimated to be the third worst hour in the month. |
|
|
76
260
|
| 4 | This hour is estimated to be the worst today, but not one of the top 3 this month. |
|
|
77
261
|
|
|
78
|
-
::: warning
|
|
262
|
+
::: warning Max ranking value
|
|
79
263
|
The highest value here, 4, is based on the value of `MAX_COUNTING` in the `Find highest per day` node.
|
|
80
264
|
If you change `MAX_COUNTING`, the values here will also change.
|
|
81
265
|
:::
|
|
82
266
|
|
|
83
|
-
###
|
|
267
|
+
### Monthly Estimate
|
|
268
|
+
|
|
269
|
+
The average of the 3 worst hours so far this month, in kWh.
|
|
270
|
+
|
|
271
|
+
### Highest Today
|
|
272
|
+
|
|
273
|
+
The consumption in kWh of the worst hour today, not including the current hour.
|
|
274
|
+
|
|
275
|
+
### Highest Today Hour
|
|
276
|
+
|
|
277
|
+
The start time of the worst hour today (Highest Today).
|
|
84
278
|
|
|
85
|
-
|
|
86
|
-
| ----- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
87
|
-
| 0 | There is nothing to worry about. |
|
|
88
|
-
| 1 | The estimate for the current hour sets this as the second to worst hour this month, but it is still way under the next step |
|
|
89
|
-
| 2 | The estimate for the current hour sets this as the worst hour this month, but it is still way under the next step |
|
|
90
|
-
| 3 | The estimate for the current hour is close to the limit for the next step |
|
|
91
|
-
| 4 | The estimate for the current hour is very close to the limit for the next step |
|
|
92
|
-
| 5 | The estimate for the current hour is over the limit for the next step |
|
|
93
|
-
| 6 | The estimate for the current hour is way over the limit for the next step |
|
|
94
|
-
| 6 | This hour increases the estimate for the month |
|
|
279
|
+
### Reduction Required
|
|
95
280
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
highestCounting (1, 2, 3)
|
|
99
|
-
highestCountingAverage
|
|
100
|
-
nextStep
|
|
281
|
+
The number of kW you must reduce the rest of the hour in order to not get alarm (break the limit),
|
|
282
|
+
based on `hourEstimate`.
|
|
101
283
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
maxToday < hourEstimate < highestCounting[1]
|
|
107
|
-
maxToday < hourEstimate < highestCounting[0]
|
|
284
|
+
This follows the `Alarm` state, and thus also the `ALARM` constant, so if
|
|
285
|
+
alarm level `8` gives status `Alarm`, reduction required is also set when the alarm level is `8`.
|
|
286
|
+
However, if you reconfigure `ALARM` to be only on level `9`, then reduction required is only set when
|
|
287
|
+
the alarm level is `9`.
|
|
108
288
|
|
|
109
|
-
|
|
289
|
+
Please note that this takes the time in consideration, so if the `hourEstimate` is 1 kWh
|
|
290
|
+
over the limit, and the time left of the hour is 15 minutes, the reduction required will
|
|
291
|
+
be 4 kW. You must reduce the consumption with 4kW in order to save 1 kWh during 15 minutes.
|
|
110
292
|
|
|
111
|
-
|
|
293
|
+
### Reduction Recommended
|
|
294
|
+
|
|
295
|
+
The number of kW you must reduce the rest of the hour in order to stay under alarm level 3,
|
|
296
|
+
based on `hourEstimate`. Calculated the same way as reduction required.
|
|
297
|
+
|
|
298
|
+
### Increase Possible
|
|
299
|
+
|
|
300
|
+
The number of kW you can increase consumption and still stay within the configured safe zone.
|
|
301
|
+
If alarm level is > 2, this value will be 0. You will never have increase possible at the same time as
|
|
302
|
+
reduction recommended (nor reduction required).
|
|
303
|
+
|
|
304
|
+
### Estimate Rest Of Hour
|
|
305
|
+
|
|
306
|
+
The estimated consumption in kWh for the rest of the hour.
|
|
307
|
+
|
|
308
|
+
### Consumption Accumulated Hour
|
|
309
|
+
|
|
310
|
+
The actual consumption in kWh until now this hour.
|
|
311
|
+
|
|
312
|
+
### Time Left
|
|
313
|
+
|
|
314
|
+
The time left of the current hour in seconds.
|
|
315
|
+
|
|
316
|
+
### Consumption Now
|
|
317
|
+
|
|
318
|
+
The current consumption, measured as average the last minute.
|
|
319
|
+
Can be set to another number of minutes using the `ESTIMATION_TIME_MINUTES` constant in
|
|
320
|
+
the `Collect estimate for hour` node.
|
|
112
321
|
|
|
113
322
|
## Node description
|
|
114
323
|
|
|
324
|
+
Here is a description of each node in the example.
|
|
325
|
+
|
|
115
326
|
### Get live data
|
|
116
327
|
|
|
117
328
|
This is a `tibber-feed` node. It sets up a subscription of live Tibber data from Tibber Pulse, and uses this data to run the automation. Tick the following check boxes:
|
|
@@ -142,6 +353,69 @@ This is a function node that is used to build a Tibber query. It runs for all th
|
|
|
142
353
|
This node needs the tibber home id, so you must find it in the [Tibber Developer Pages](https://developer.tibber.com/) and set the vale of `TIBBER_HOME_ID` in the beginning of the code.
|
|
143
354
|
:::
|
|
144
355
|
|
|
356
|
+
::: details Code
|
|
357
|
+
|
|
358
|
+
<CodeGroup>
|
|
359
|
+
<CodeGroupItem title="On Start">
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
context.set("previousHour", undefined);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
</CodeGroupItem>
|
|
366
|
+
|
|
367
|
+
<CodeGroupItem title="On Message" active>
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
/*
|
|
371
|
+
Calculate number of hours to receive consumption for,
|
|
372
|
+
that is number of hours in the month until now.
|
|
373
|
+
Constructs a tibber query to get consumption per hour.
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
const TIBBER_HOME_ID = "put your tibber ome id here";
|
|
377
|
+
|
|
378
|
+
const timestamp = msg.payload.timestamp;
|
|
379
|
+
|
|
380
|
+
// Stop if hour has not changed
|
|
381
|
+
const time = new Date(timestamp);
|
|
382
|
+
const hour = time.getHours();
|
|
383
|
+
const previousHour = context.get("previousHour");
|
|
384
|
+
if (previousHour !== undefined && hour === previousHour) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
context.set("previousHour", hour);
|
|
388
|
+
|
|
389
|
+
// Calculate number of hours to query
|
|
390
|
+
const date = time.getDate() - 1;
|
|
391
|
+
const hour2 = time.getHours();
|
|
392
|
+
const count = date * 24 + hour2;
|
|
393
|
+
|
|
394
|
+
// Build query
|
|
395
|
+
const query = `
|
|
396
|
+
{
|
|
397
|
+
viewer {
|
|
398
|
+
home (id: "${TIBBER_HOME_ID}") {
|
|
399
|
+
consumption(resolution: HOURLY, last: ${count}) {
|
|
400
|
+
nodes {
|
|
401
|
+
from
|
|
402
|
+
consumption
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
msg.payload = query;
|
|
411
|
+
return msg;
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
</CodeGroupItem>
|
|
415
|
+
</CodeGroup>
|
|
416
|
+
|
|
417
|
+
:::
|
|
418
|
+
|
|
145
419
|
### Get consumption
|
|
146
420
|
|
|
147
421
|
This is a `tibber-query` node used to get consumption per hour for passed hours. It takes a Tibber query as input, and sends the result as output. The query is built by the previous node.
|
|
@@ -186,24 +460,104 @@ It must contain data for every hour from the beginning of the month until the la
|
|
|
186
460
|
|
|
187
461
|
### Collect estimate for hour
|
|
188
462
|
|
|
189
|
-
This is a function node that receives all the live data and estimates the consumption for the rest of the current hour.
|
|
463
|
+
This is a function node that receives all the live data and estimates the consumption for the rest of the current hour.
|
|
464
|
+
It sums up the actual consumption from the beginning of the hour until the current time,
|
|
465
|
+
and adds the estimate for the rest of the hour, giving a total estimate for the hour.
|
|
190
466
|
|
|
191
467
|
In the beginning of the code, there is a constant `ESTIMATION_TIME_MINUTES` that you can use to decide how many minutes that is used
|
|
192
468
|
to calculate assumed average consumption.
|
|
193
469
|
|
|
194
470
|
The function keeps a buffer with all readings for the last period of length given with `ESTIMATION_TIME_MINUTES`.
|
|
195
|
-
It uses this buffer to calculate the average consumption for the period.
|
|
471
|
+
It uses this buffer to calculate the average consumption for the period.
|
|
472
|
+
It uses the result and estimates the consumption for the rest of the hour, assuming that the consumption will be the same.
|
|
196
473
|
|
|
197
474
|
As outputs it sends the following:
|
|
198
475
|
|
|
199
|
-
| Name
|
|
200
|
-
|
|
|
201
|
-
| accumulatedConsumption
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
476
|
+
| Name | Description |
|
|
477
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
478
|
+
| `accumulatedConsumption` | Accumulated consumption the current day |
|
|
479
|
+
| `accumulatedConsumptionLastHour` | Accumulated consumption the current hour. |
|
|
480
|
+
| `periodMs` | Period the average is calculated for, in milliseconds. It will increase in the beginning, until it reaches `ESTIMATION_TIME_MINUTES * 60 * 1000` |
|
|
481
|
+
| `consumptionInPeriod` | Consumption in the last `periodMs` milliseconds. Used as estimate for the remaining of the hour. |
|
|
482
|
+
| `averageConsumptionNow` | Consumption the last minute (or `ESTIMATION_TIME_MINUTES` minutes). |
|
|
483
|
+
| `timeLeftMs` | Number of milliseconds left in the current hour. |
|
|
484
|
+
| `consumptionLeft` | Estimated consumption the remaining of the hour. |
|
|
485
|
+
| `hourEstimate` | The estimated consumption for the total hour. |
|
|
486
|
+
| `currentHour` | The time of the current hour. |
|
|
487
|
+
|
|
488
|
+
::: details Code
|
|
489
|
+
|
|
490
|
+
<CodeGroup>
|
|
491
|
+
<CodeGroupItem title="On Start">
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
context.set("buffer", []);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
</CodeGroupItem>
|
|
498
|
+
|
|
499
|
+
<CodeGroupItem title="On Message" active>
|
|
500
|
+
|
|
501
|
+
```js
|
|
502
|
+
// Number of minutes used to calculate assumed consumption:
|
|
503
|
+
const ESTIMATION_TIME_MINUTES = 1;
|
|
504
|
+
|
|
505
|
+
const buffer = context.get("buffer") || [];
|
|
506
|
+
|
|
507
|
+
// Add new record to buffer
|
|
508
|
+
const time = new Date(msg.payload.timestamp);
|
|
509
|
+
const timeMs = time.getTime();
|
|
510
|
+
const accumulatedConsumption = msg.payload.accumulatedConsumption;
|
|
511
|
+
const accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour;
|
|
512
|
+
buffer.push({ timeMs, accumulatedConsumption });
|
|
513
|
+
|
|
514
|
+
const currentHour = new Date(msg.payload.timestamp);
|
|
515
|
+
currentHour.setMinutes(0);
|
|
516
|
+
currentHour.setSeconds(0);
|
|
517
|
+
|
|
518
|
+
// Remove too old records from buffer
|
|
519
|
+
const maxAgeMs = ESTIMATION_TIME_MINUTES * 60 * 1000;
|
|
520
|
+
let oldest = buffer[0];
|
|
521
|
+
while (timeMs - oldest.timeMs > maxAgeMs) {
|
|
522
|
+
buffer.splice(0, 1);
|
|
523
|
+
oldest = buffer[0];
|
|
524
|
+
}
|
|
525
|
+
context.set("buffer", buffer);
|
|
526
|
+
|
|
527
|
+
// Calculate buffer
|
|
528
|
+
const periodMs = buffer[buffer.length - 1].timeMs - buffer[0].timeMs;
|
|
529
|
+
const consumptionInPeriod = buffer[buffer.length - 1].accumulatedConsumption - buffer[0].accumulatedConsumption;
|
|
530
|
+
if (periodMs === 0) {
|
|
531
|
+
return; // First item in buffer
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Estimate remaining of current hour
|
|
535
|
+
const timeLeftMs = 60 * 60 * 1000 - (time.getMinutes() * 60000 + time.getSeconds() * 1000 + time.getMilliseconds());
|
|
536
|
+
const consumptionLeft = (consumptionInPeriod / periodMs) * timeLeftMs;
|
|
537
|
+
const averageConsumptionNow = (consumptionInPeriod / periodMs) * 60 * 60 * 1000;
|
|
538
|
+
|
|
539
|
+
// Estimate total hour
|
|
540
|
+
const hourEstimate = accumulatedConsumptionLastHour + consumptionLeft + 0; // Change for testing
|
|
541
|
+
|
|
542
|
+
msg.payload = {
|
|
543
|
+
accumulatedConsumption,
|
|
544
|
+
accumulatedConsumptionLastHour,
|
|
545
|
+
periodMs,
|
|
546
|
+
consumptionInPeriod,
|
|
547
|
+
averageConsumptionNow,
|
|
548
|
+
timeLeftMs,
|
|
549
|
+
consumptionLeft,
|
|
550
|
+
hourEstimate,
|
|
551
|
+
currentHour,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
return msg;
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
</CodeGroupItem>
|
|
558
|
+
</CodeGroup>
|
|
559
|
+
|
|
560
|
+
:::
|
|
207
561
|
|
|
208
562
|
### Find highest per day
|
|
209
563
|
|
|
@@ -218,55 +572,582 @@ Based on the result from the tibber query, gives the following output:
|
|
|
218
572
|
|
|
219
573
|
Output is sent when the query is run, that is on startup and when the hour changes.
|
|
220
574
|
|
|
221
|
-
|
|
575
|
+
::: details Code
|
|
576
|
+
|
|
577
|
+
<CodeGroup>
|
|
578
|
+
<CodeGroupItem title="On Message" active>
|
|
579
|
+
|
|
580
|
+
```js
|
|
581
|
+
const MAX_COUNTING = 3;
|
|
582
|
+
const hours = msg.payload.viewer.home.consumption.nodes;
|
|
583
|
+
const days = new Map();
|
|
584
|
+
hours.forEach((h) => {
|
|
585
|
+
const date = new Date(h.from).getDate();
|
|
586
|
+
if (!days.has(date) || h.consumption > days.get(date).consumption) {
|
|
587
|
+
days.set(date, { from: h.from, consumption: h.consumption });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
const highestToday = days.get(new Date().getDate()) ?? 0;
|
|
591
|
+
const highestPerDay = [...days.values()].sort((a, b) => b.consumption - a.consumption);
|
|
592
|
+
const highestCounting = highestPerDay.slice(0, MAX_COUNTING);
|
|
593
|
+
const currentMonthlyMaxAverage =
|
|
594
|
+
highestCounting.length === 0
|
|
595
|
+
? 0
|
|
596
|
+
: highestCounting.reduce((prev, val) => prev + val.consumption, 0) / highestCounting.length;
|
|
597
|
+
msg.payload = {
|
|
598
|
+
highestPerDay,
|
|
599
|
+
highestCounting,
|
|
600
|
+
highestToday,
|
|
601
|
+
currentMonthlyMaxAverage,
|
|
602
|
+
};
|
|
603
|
+
return msg;
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
</CodeGroupItem>
|
|
607
|
+
</CodeGroup>
|
|
608
|
+
|
|
609
|
+
:::
|
|
610
|
+
|
|
611
|
+
### Calculate values
|
|
612
|
+
|
|
613
|
+
This is where calculation is done to produce all the output sensor values.
|
|
614
|
+
|
|
615
|
+
In the beginning of the script there are some constants you can configure:
|
|
616
|
+
|
|
617
|
+
```js
|
|
618
|
+
const HA_NAME = "homeAssistant"; // Your HA name
|
|
619
|
+
const STEPS = [2, 5, 10, 15, 20]; // Grid tariff steps in kWh
|
|
620
|
+
const MAX_COUNTING = 3; // Number of days to calculate month average of
|
|
621
|
+
const BUFFER = 0.5; // kWh - Closer to limit increases alarm level
|
|
622
|
+
const SAFE_SONE = 2; // kWh - Further from limit reduces level
|
|
623
|
+
const ALARM = 8; // Min level that causes status to be alarm
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The `HA_NAME` must be set to the name you have given your Home Assistant. One place to find this is in Node-RED,
|
|
627
|
+
in the `Context Data` window (next to the `Debug` window), under `Global`, click the refresh button and see the `homeassistant` object.
|
|
628
|
+
Find the name used to the right.
|
|
629
|
+
In this example the value you are looking for is `homeAssistant`:
|
|
630
|
+
|
|
631
|
+

|
|
632
|
+
|
|
633
|
+
You must configure the `STEPS` array to contain steps relevant for you.
|
|
634
|
+
You should omit steps you do not plan to go under, to avoid non-necessary actions and warnings.
|
|
635
|
+
|
|
636
|
+
See [Calculated sensor values](#calculated-sensor-values) for description of the output.
|
|
637
|
+
|
|
638
|
+
::: details Code
|
|
639
|
+
|
|
640
|
+
<CodeGroup>
|
|
641
|
+
<CodeGroupItem title="On Message" active>
|
|
642
|
+
|
|
643
|
+
```js
|
|
644
|
+
const HA_NAME = "homeAssistant"; // Your HA name
|
|
645
|
+
const STEPS = [2, 5, 10, 15, 20];
|
|
646
|
+
const MAX_COUNTING = 3; // Number of days to calculate month
|
|
647
|
+
const BUFFER = 0.5; // Closer to limit increases level
|
|
648
|
+
const SAFE_ZONE = 2; // Further from limit reduces level
|
|
649
|
+
const ALARM = 8; // Min level that causes status to be alarm
|
|
650
|
+
|
|
651
|
+
const ha = global.get("homeassistant")[HA_NAME];
|
|
652
|
+
if (!ha.isConnected) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isNull(value) {
|
|
657
|
+
return value === null || value === undefined;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function calculateLevel(hourEstimate, currentHourRanking, highestCountingAverageWithCurrent, nextStep) {
|
|
661
|
+
if (currentHourRanking === 0) {
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
if (highestCountingAverageWithCurrent > nextStep) {
|
|
665
|
+
return 9;
|
|
666
|
+
}
|
|
667
|
+
if (highestCountingAverageWithCurrent > nextStep - BUFFER) {
|
|
668
|
+
return 8;
|
|
669
|
+
}
|
|
670
|
+
if (hourEstimate > nextStep) {
|
|
671
|
+
return 7;
|
|
672
|
+
}
|
|
673
|
+
if (hourEstimate > nextStep - BUFFER) {
|
|
674
|
+
return 6;
|
|
675
|
+
}
|
|
676
|
+
if (currentHourRanking === 1 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
677
|
+
return 5;
|
|
678
|
+
}
|
|
679
|
+
if (currentHourRanking === 2 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
680
|
+
return 4;
|
|
681
|
+
}
|
|
682
|
+
if (currentHourRanking === 3 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
683
|
+
return 3;
|
|
684
|
+
}
|
|
685
|
+
if (currentHourRanking === 1) {
|
|
686
|
+
return 2;
|
|
687
|
+
}
|
|
688
|
+
if (currentHourRanking === 2) {
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (msg.payload.highestPerDay) {
|
|
695
|
+
context.set("highestPerDay", msg.payload.highestPerDay);
|
|
696
|
+
context.set("highestCounting", msg.payload.highestCounting);
|
|
697
|
+
context.set("highestToday", msg.payload.highestToday);
|
|
698
|
+
context.set("currentMonthlyMaxAverage", msg.payload.currentMonthlyMaxAverage);
|
|
699
|
+
node.status({ fill: "green", shape: "ring", text: "Got ranking" });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const highestPerDay = context.get("highestPerDay");
|
|
704
|
+
const highestCounting = context.get("highestCounting");
|
|
705
|
+
const highestToday = context.get("highestToday");
|
|
706
|
+
const currentMonthlyMaxAverage = context.get("currentMonthlyMaxAverage");
|
|
707
|
+
const hourEstimate = msg.payload.hourEstimate;
|
|
708
|
+
const timeLeftMs = msg.payload.timeLeftMs;
|
|
709
|
+
const timeLeftSec = timeLeftMs / 1000;
|
|
710
|
+
const periodMs = msg.payload.periodMs;
|
|
711
|
+
const accumulatedConsumption = msg.payload.accumulatedConsumption;
|
|
712
|
+
const accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour;
|
|
713
|
+
const consumptionLeft = msg.payload.consumptionLeft;
|
|
714
|
+
const averageConsumptionNow = msg.payload.averageConsumptionNow;
|
|
715
|
+
const currentHour = msg.payload.currentHour;
|
|
716
|
+
|
|
717
|
+
if (timeLeftSec === 0) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (isNull(highestPerDay)) {
|
|
722
|
+
node.status({ fill: "red", shape: "dot", text: "No highest per day" });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (isNull(highestToday)) {
|
|
726
|
+
node.status({ fill: "red", shape: "dot", text: "No highest today" });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (isNull(hourEstimate)) {
|
|
730
|
+
node.status({ fill: "red", shape: "dot", text: "No estimate" });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const currentStep = STEPS.reduceRight(
|
|
735
|
+
(prev, val) => (val > currentMonthlyMaxAverage ? val : prev),
|
|
736
|
+
STEPS[STEPS.length - 1]
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// Set currentHourRanking
|
|
740
|
+
let currentHourRanking = MAX_COUNTING + 1;
|
|
741
|
+
for (let i = highestCounting.length - 1; i >= 0; i--) {
|
|
742
|
+
if (hourEstimate > highestCounting[i].consumption) {
|
|
743
|
+
currentHourRanking = i + 1;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (hourEstimate < highestToday.consumption) {
|
|
747
|
+
currentHourRanking = 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const current = { from: currentHour, consumption: hourEstimate };
|
|
751
|
+
const highestCountingWithCurrent = [...highestCounting, current]
|
|
752
|
+
.sort((a, b) => b.consumption - a.consumption)
|
|
753
|
+
.slice(0, highestCounting.length);
|
|
754
|
+
const currentMonthlyEstimate =
|
|
755
|
+
highestCountingWithCurrent.length === 0
|
|
756
|
+
? 0
|
|
757
|
+
: highestCountingWithCurrent.reduce((prev, val) => prev + val.consumption, 0) / highestCountingWithCurrent.length;
|
|
758
|
+
|
|
759
|
+
// Set alarm level
|
|
760
|
+
const alarmLevel = calculateLevel(hourEstimate, currentHourRanking, currentMonthlyEstimate, currentStep);
|
|
761
|
+
|
|
762
|
+
// Evaluate status
|
|
763
|
+
const status = alarmLevel >= ALARM ? "Alarm" : alarmLevel > 0 ? "Warning" : "Ok";
|
|
764
|
+
|
|
765
|
+
// Calculate reduction
|
|
766
|
+
const reductionRequired =
|
|
767
|
+
alarmLevel < ALARM
|
|
768
|
+
? 0
|
|
769
|
+
: (Math.max((currentMonthlyEstimate - currentStep) * highestCounting.length, 0) * 3600) / timeLeftSec;
|
|
770
|
+
const reductionRecommended =
|
|
771
|
+
alarmLevel < 3 ? 0 : (Math.max(hourEstimate + SAFE_ZONE - currentStep, 0) * 3600) / timeLeftSec;
|
|
772
|
+
|
|
773
|
+
// Calculate increase possible
|
|
774
|
+
const increasePossible =
|
|
775
|
+
alarmLevel >= 3 ? 0 : (Math.max(currentStep - hourEstimate - SAFE_ZONE, 0) * 3600) / timeLeftSec;
|
|
776
|
+
|
|
777
|
+
// Create output
|
|
778
|
+
const fill = status === "Ok" ? "green" : status === "Alarm" ? "red" : "yellow";
|
|
779
|
+
node.status({ fill, shape: "dot", text: "Working" });
|
|
780
|
+
|
|
781
|
+
const RESOLUTION = 1000;
|
|
782
|
+
|
|
783
|
+
const payload = {
|
|
784
|
+
status, // Ok, Warning, Alarm
|
|
785
|
+
statusOk: status === "Ok",
|
|
786
|
+
statusWarning: status === "Warning",
|
|
787
|
+
statusAlarm: status === "Alarm",
|
|
788
|
+
alarmLevel,
|
|
789
|
+
highestPerDay,
|
|
790
|
+
highestCounting,
|
|
791
|
+
highestCountingWithCurrent,
|
|
792
|
+
highestToday,
|
|
793
|
+
currentMonthlyEstimate: Math.round(currentMonthlyEstimate * RESOLUTION) / RESOLUTION,
|
|
794
|
+
accumulatedConsumptionLastHour: Math.round(accumulatedConsumptionLastHour * RESOLUTION) / RESOLUTION,
|
|
795
|
+
consumptionLeft: Math.round(consumptionLeft * RESOLUTION) / RESOLUTION,
|
|
796
|
+
hourEstimate: Math.round(hourEstimate * RESOLUTION) / RESOLUTION,
|
|
797
|
+
averageConsumptionNow: Math.round(averageConsumptionNow * RESOLUTION) / RESOLUTION,
|
|
798
|
+
reductionRequired: Math.round(reductionRequired * RESOLUTION) / RESOLUTION,
|
|
799
|
+
reductionRecommended: Math.round(reductionRecommended * RESOLUTION) / RESOLUTION,
|
|
800
|
+
increasePossible: Math.round(increasePossible * RESOLUTION) / RESOLUTION,
|
|
801
|
+
currentStep,
|
|
802
|
+
currentHourRanking,
|
|
803
|
+
timeLeftSec,
|
|
804
|
+
periodMs,
|
|
805
|
+
accumulatedConsumption,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
msg.payload = payload;
|
|
809
|
+
|
|
810
|
+
return msg;
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
</CodeGroupItem>
|
|
814
|
+
</CodeGroup>
|
|
815
|
+
|
|
816
|
+
:::
|
|
817
|
+
|
|
818
|
+
### Reduction Actions
|
|
819
|
+
|
|
820
|
+
This is where you set up actions to be taken in case reduction is required or recommended.
|
|
821
|
+
|
|
822
|
+
::: danger Configure actions
|
|
823
|
+
You must set up your own actions in this script, if you are going to use actions.
|
|
824
|
+
:::
|
|
825
|
+
|
|
826
|
+
In the **On Start** tab of this node, you set up the actions by writing a Javascript array,
|
|
827
|
+
the `actions` array.
|
|
828
|
+
The example shows some actions, but you may set up any number of actions.
|
|
829
|
+
|
|
830
|
+
Actions are taken by sending a payload to a HA `call service` node (the `Perform action` node).
|
|
831
|
+
The items in the `actions` array contains the payload you need to send to the `call service` node
|
|
832
|
+
in order to take action, and the payload you need to send to the same `call service` node
|
|
833
|
+
in order to reset the action.
|
|
834
|
+
|
|
835
|
+
An action may be to turn on or off a switch, to perform a climate control or what ever else
|
|
836
|
+
you can do to control your entities.
|
|
837
|
+
|
|
838
|
+
In order to know how much power that is saved by turning off an action, each item must have a sensor
|
|
839
|
+
to give this information. This way, if a device is not using any power, the action will not be taken.
|
|
840
|
+
|
|
841
|
+
::: warning Entity consumption
|
|
842
|
+
In the current example, there must be a sensor holding the consumption of the entity to
|
|
843
|
+
turn off. If this is not possible, the code must be changed in order to work.
|
|
844
|
+
:::
|
|
845
|
+
|
|
846
|
+
Each item in the `actions` array contains the following data:
|
|
847
|
+
|
|
848
|
+
| Variable name | Description |
|
|
849
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
850
|
+
| consumption | The consumption that will be reduced by taking the action, given as either a) (Recommended) The entity_id of a sensor that gives the consumption, or b) A number with the consumption in kWh, or c) a function returning the consumption. |
|
|
851
|
+
| name | The name of the actions. Can be any thing. |
|
|
852
|
+
| id | A unique id of the action. |
|
|
853
|
+
| minAlarmLevel | The minimum alarm level that must be present to take this action. |
|
|
854
|
+
| reduceWhenRecommended | If `true` the action will be taken when `Reduction Recommended` > 0. If `false` the action will be taken only when `Reduction Required` > 0 |
|
|
855
|
+
| minTimeOffSec | The action will not be reset until minimum this number of seconds has passed since the action was taken. |
|
|
856
|
+
| payloadToTakeAction | The payload that shall be sent to the `call service` node to take the action (for example turn off a switch). |
|
|
857
|
+
| payloadToResetAction | The payload that shall be sent to the `call service` node to reset the action (for example turn a switch back on again). |
|
|
858
|
+
|
|
859
|
+
::: tip Actions order
|
|
860
|
+
Actions to reduce consumption are taken in the order they appear in the `actions` array until enough reduction has been done,
|
|
861
|
+
so put first the actions you want to take first, and last those actions that you want to take only when really necessary.
|
|
862
|
+
:::
|
|
863
|
+
|
|
864
|
+
Here is an example of an `actions` array with two items (a water heater and a heating cable):
|
|
865
|
+
|
|
866
|
+
::: danger On Start code
|
|
867
|
+
Please note that there is a small piece of code after the `actions` array
|
|
868
|
+
in the `On Start` tab. Make sure you do not delete that code.
|
|
869
|
+
:::
|
|
870
|
+
|
|
871
|
+
::: tip Sensors without actions
|
|
872
|
+
If you don't want the actions, or you want to control actions another way,
|
|
873
|
+
you can omit the action-related nodes and only use the nodes creating the sensors.
|
|
874
|
+
:::
|
|
875
|
+
|
|
876
|
+
::: details Code
|
|
877
|
+
|
|
878
|
+
<CodeGroup>
|
|
879
|
+
<CodeGroupItem title="On Start">
|
|
880
|
+
|
|
881
|
+
```js
|
|
882
|
+
// You MUST edit the actions array with your own actions.
|
|
883
|
+
|
|
884
|
+
const actions = [
|
|
885
|
+
{
|
|
886
|
+
consumption: "sensor.varmtvannsbereder_electric_consumption_w",
|
|
887
|
+
name: "Varmtvannsbereder",
|
|
888
|
+
id: "vvb",
|
|
889
|
+
minAlarmLevel: 3,
|
|
890
|
+
reduceWhenRecommended: true,
|
|
891
|
+
minTimeOffSec: 300,
|
|
892
|
+
payloadToTakeAction: {
|
|
893
|
+
domain: "switch",
|
|
894
|
+
service: "turn_off",
|
|
895
|
+
target: {
|
|
896
|
+
entity_id: ["switch.varmtvannsbereder"],
|
|
897
|
+
},
|
|
898
|
+
},
|
|
899
|
+
payloadToResetAction: {
|
|
900
|
+
domain: "switch",
|
|
901
|
+
service: "turn_on",
|
|
902
|
+
target: {
|
|
903
|
+
entity_id: ["switch.varmtvannsbereder"],
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
consumption: "sensor.varme_gulv_bad_electric_consumption_w_2",
|
|
909
|
+
name: "Varme gulv bad 1. etg.",
|
|
910
|
+
id: "gulvbad",
|
|
911
|
+
minAlarmLevel: 3,
|
|
912
|
+
reduceWhenRecommended: true,
|
|
913
|
+
minTimeOffSec: 300,
|
|
914
|
+
payloadToTakeAction: {
|
|
915
|
+
domain: "climate",
|
|
916
|
+
service: "turn_off",
|
|
917
|
+
target: {
|
|
918
|
+
entity_id: ["climate.varme_gulv_bad_2"],
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
payloadToResetAction: {
|
|
922
|
+
domain: "climate",
|
|
923
|
+
service: "turn_on",
|
|
924
|
+
target: {
|
|
925
|
+
entity_id: ["climate.varme_gulv_bad_2"],
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
consumption: "sensor.varme_gulv_gang_electric_consumption_w",
|
|
931
|
+
name: "Varme gulv gang 1. etg.",
|
|
932
|
+
id: "gulvgang",
|
|
933
|
+
minAlarmLevel: 3,
|
|
934
|
+
reduceWhenRecommended: true,
|
|
935
|
+
minTimeOffSec: 300,
|
|
936
|
+
payloadToTakeAction: {
|
|
937
|
+
domain: "climate",
|
|
938
|
+
service: "turn_off",
|
|
939
|
+
target: {
|
|
940
|
+
entity_id: ["climate.varme_gulv_gang"],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
payloadToResetAction: {
|
|
944
|
+
domain: "climate",
|
|
945
|
+
service: "turn_on",
|
|
946
|
+
target: {
|
|
947
|
+
entity_id: ["climate.varme_gulv_gang"],
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
consumption: "sensor.varme_gulv_kjellerstue_electric_consumption_w",
|
|
953
|
+
name: "Varme gulv kjellerstue",
|
|
954
|
+
id: "gulvkjeller",
|
|
955
|
+
minAlarmLevel: 3,
|
|
956
|
+
reduceWhenRecommended: true,
|
|
957
|
+
minTimeOffSec: 300,
|
|
958
|
+
payloadToTakeAction: {
|
|
959
|
+
domain: "climate",
|
|
960
|
+
service: "turn_off",
|
|
961
|
+
target: {
|
|
962
|
+
entity_id: ["climate.varme_gulv_kjellerstue"],
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
payloadToResetAction: {
|
|
966
|
+
domain: "climate",
|
|
967
|
+
service: "turn_on",
|
|
968
|
+
target: {
|
|
969
|
+
entity_id: ["climate.varme_gulv_kjellerstue"],
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
consumption: 0.1,
|
|
975
|
+
name: "Test",
|
|
976
|
+
id: "test",
|
|
977
|
+
minAlarmLevel: 3,
|
|
978
|
+
reduceWhenRecommended: true,
|
|
979
|
+
minTimeOffSec: 30,
|
|
980
|
+
payloadToTakeAction: {
|
|
981
|
+
domain: "switch",
|
|
982
|
+
service: "turn_off",
|
|
983
|
+
target: {
|
|
984
|
+
entity_id: ["switch.lys_kjokkenskap_switch"],
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
payloadToResetAction: {
|
|
988
|
+
domain: "switch",
|
|
989
|
+
service: "turn_on",
|
|
990
|
+
target: {
|
|
991
|
+
entity_id: ["switch.lys_kjokkenskap_switch"],
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
];
|
|
996
|
+
// End of actions array
|
|
997
|
+
|
|
998
|
+
// DO NOT DELETE THE CODE BELOW
|
|
999
|
+
|
|
1000
|
+
// Set default values for all actions
|
|
1001
|
+
actions.forEach((a) => {
|
|
1002
|
+
a.actionTaken = false;
|
|
1003
|
+
a.savedConsumption = 0;
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
flow.set("actions", actions);
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
</CodeGroupItem>
|
|
1010
|
+
|
|
1011
|
+
<CodeGroupItem title="On Message" active>
|
|
1012
|
+
|
|
1013
|
+
```js
|
|
1014
|
+
const MIN_CONSUMPTION_TO_CARE = 0.05; // Do not reduce unless at least 50W
|
|
1015
|
+
|
|
1016
|
+
const actions = flow.get("actions");
|
|
1017
|
+
const ha = global.get("homeassistant").homeAssistant;
|
|
1018
|
+
|
|
1019
|
+
let reductionRequired = msg.payload.reductionRequired;
|
|
1020
|
+
let reductionRecommended = msg.payload.reductionRecommended;
|
|
1021
|
+
|
|
1022
|
+
if (reductionRecommended <= 0) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function takeAction(action, consumption) {
|
|
1027
|
+
const info = {
|
|
1028
|
+
time: new Date().toISOString(),
|
|
1029
|
+
name: "Reduction action",
|
|
1030
|
+
data: msg.payload,
|
|
1031
|
+
action,
|
|
1032
|
+
};
|
|
1033
|
+
node.send([{ payload: action.payloadToTakeAction }, { payload: info }]);
|
|
1034
|
+
reductionRequired = Math.max(0, reductionRequired - consumption);
|
|
1035
|
+
reductionRecommended = Math.max(0, reductionRecommended - consumption);
|
|
1036
|
+
action.actionTaken = true;
|
|
1037
|
+
action.actionTime = Date.now();
|
|
1038
|
+
action.savedConsumption = consumption;
|
|
1039
|
+
flow.set("actions", actions);
|
|
1040
|
+
}
|
|
222
1041
|
|
|
223
|
-
|
|
224
|
-
|
|
1042
|
+
function getConsumption(consumption) {
|
|
1043
|
+
if (typeof consumption === "string") {
|
|
1044
|
+
const sensor = ha.states[consumption];
|
|
1045
|
+
return sensor.state;
|
|
1046
|
+
} else if (typeof consumption === "number") {
|
|
1047
|
+
return consumption;
|
|
1048
|
+
} else if (typeof consumption === "function") {
|
|
1049
|
+
return consumption();
|
|
1050
|
+
} else {
|
|
1051
|
+
node.warn("Config error: consumption has illegal type: " + typeof consumption);
|
|
1052
|
+
return 0;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
225
1055
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1056
|
+
actions
|
|
1057
|
+
.filter((a) => msg.payload.alarmLevel >= a.minAlarmLevel && !a.actionTaken)
|
|
1058
|
+
.forEach((a) => {
|
|
1059
|
+
const consumption = getConsumption(a.consumption);
|
|
1060
|
+
if (consumption < MIN_CONSUMPTION_TO_CARE) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (reductionRequired > 0 || (reductionRecommended > 0 && a.reduceWhenRecommended)) {
|
|
1064
|
+
takeAction(a, consumption);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
```
|
|
237
1068
|
|
|
238
|
-
|
|
1069
|
+
</CodeGroupItem>
|
|
1070
|
+
</CodeGroup>
|
|
239
1071
|
|
|
240
|
-
|
|
241
|
-
For each action, specify priority and expected reduction in kW,
|
|
242
|
-
as well as what data to send as output for that action.
|
|
1072
|
+
:::
|
|
243
1073
|
|
|
244
|
-
|
|
245
|
-
Actions are taken in prioritized order, until enough reduction is expected,
|
|
246
|
-
so multiple actions may be taken at the same time.
|
|
1074
|
+
### Reset Actions
|
|
247
1075
|
|
|
248
|
-
|
|
249
|
-
The first action has highest priority, that is it will be executed first.
|
|
250
|
-
The rest follow in prioritized order.
|
|
1076
|
+
This node will reset actions when there is enough capacity available, that is for example turning switches back on again.
|
|
251
1077
|
|
|
252
|
-
|
|
1078
|
+
In the script, there is a `BUFFER_TO_RESET` constant used to set a buffer (in kW) so actions are not reset until there is
|
|
1079
|
+
some spare capacity. By default is it set to 1 kW.
|
|
253
1080
|
|
|
254
|
-
|
|
255
|
-
- outputActivate - The data sent to the output to acticate the action.
|
|
256
|
-
- outputDeactivate - The data sent to the output to deactivate the action.
|
|
257
|
-
- activateOnWarning - true if this shall be activated on warning.
|
|
1081
|
+
::: details Code
|
|
258
1082
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
When an action is activated, the outputActivate data is sent to the corresponding output.
|
|
262
|
-
When the hour is over, the action is deactivated.
|
|
263
|
-
When an action is deativated, the outputDeactivate data is sent to the corresponding output.
|
|
1083
|
+
<CodeGroup>
|
|
1084
|
+
<CodeGroupItem title="On Message" active>
|
|
264
1085
|
|
|
265
|
-
|
|
1086
|
+
```js
|
|
1087
|
+
const actions = flow.get("actions");
|
|
1088
|
+
const ha = global.get("homeassistant").homeAssistant;
|
|
266
1089
|
|
|
267
|
-
|
|
1090
|
+
const BUFFER_TO_RESET = 1; // Must have 1kW extra to perform reset
|
|
1091
|
+
|
|
1092
|
+
let increasePossible = msg.payload.increasePossible;
|
|
1093
|
+
|
|
1094
|
+
if (increasePossible <= 0) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function resetAction(action) {
|
|
1099
|
+
const info = {
|
|
1100
|
+
time: new Date().toISOString(),
|
|
1101
|
+
name: "Reset action",
|
|
1102
|
+
data: msg.paylaod,
|
|
1103
|
+
action,
|
|
1104
|
+
};
|
|
1105
|
+
node.send([{ payload: action.payloadToResetAction }, { payload: info }]);
|
|
1106
|
+
increasePossible -= action.savedConsumption;
|
|
1107
|
+
action.actionTaken = false;
|
|
1108
|
+
action.savedConsumption = 0;
|
|
1109
|
+
flow.set("actions", actions);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
actions
|
|
1113
|
+
.filter(
|
|
1114
|
+
(a) =>
|
|
1115
|
+
a.actionTaken &&
|
|
1116
|
+
a.savedConsumption + BUFFER_TO_RESET <= increasePossible &&
|
|
1117
|
+
Date.now() - a.actionTime > a.minTimeOffSec * 1000
|
|
1118
|
+
)
|
|
1119
|
+
.forEach((a) => resetAction(a));
|
|
1120
|
+
```
|
|
268
1121
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1122
|
+
</CodeGroupItem>
|
|
1123
|
+
</CodeGroup>
|
|
1124
|
+
|
|
1125
|
+
:::
|
|
1126
|
+
|
|
1127
|
+
### Perform action
|
|
1128
|
+
|
|
1129
|
+
This is a `call service` node used to perform the actions (both taking actions and resetting actions).
|
|
1130
|
+
There is no setup here except selecting the HA server.
|
|
1131
|
+
|
|
1132
|
+
### Save actions to file
|
|
1133
|
+
|
|
1134
|
+
Saves some information to a file, when actions are taken or reset. This is just so you can watch what has been done.
|
|
1135
|
+
Please make sure the file name configured works for you (for example that the folder exists in your HA instance).
|
|
1136
|
+
|
|
1137
|
+
### Catch action errors
|
|
1138
|
+
|
|
1139
|
+
This is supposed to catch any errors in the action-related nodes, and log them to the file.
|
|
1140
|
+
|
|
1141
|
+
### Entity nodes
|
|
1142
|
+
|
|
1143
|
+
There is a large number of `entity` nodes, one for each sensor value that is created with values from the `Calculate values` node.
|
|
1144
|
+
See [Calculated sensor values](#calculated-sensor-values) for description of each sensor value.
|
|
1145
|
+
|
|
1146
|
+
## The code
|
|
1147
|
+
|
|
1148
|
+
Below is the code for the Node-RED flow.
|
|
1149
|
+
Copy the code and paste it to Node-RED using Import in the NR menu.
|
|
1150
|
+
|
|
1151
|
+
::: details Flow code
|
|
1152
|
+
@[code](./example-grid-tariff-capacity-flow.json)
|
|
1153
|
+
:::
|