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.
Files changed (107) hide show
  1. package/.eslintrc.js +15 -0
  2. package/docs/.vuepress/components/DonateButtons.vue +26 -3
  3. package/docs/.vuepress/components/VippsPlakat.vue +20 -0
  4. package/docs/.vuepress/config.js +18 -10
  5. package/docs/.vuepress/public/ads.txt +1 -0
  6. package/docs/README.md +4 -4
  7. package/docs/changelog/README.md +59 -1
  8. package/docs/contribute/README.md +8 -3
  9. package/docs/examples/README.md +2 -0
  10. package/docs/examples/example-grid-tariff-capacity-flow.json +1142 -0
  11. package/docs/examples/example-grid-tariff-capacity-part.md +988 -107
  12. package/docs/faq/README.md +1 -1
  13. package/docs/faq/best-save-viewer.md +1 -1
  14. package/docs/guide/README.md +20 -5
  15. package/docs/images/best-save-config.png +0 -0
  16. package/docs/images/combine-two-lowest-price.png +0 -0
  17. package/docs/images/example-capacity-flow.png +0 -0
  18. package/docs/images/fixed-schedule-config.png +0 -0
  19. package/docs/images/global-context-window.png +0 -0
  20. package/docs/images/lowest-price-config.png +0 -0
  21. package/docs/images/node-ps-schedule-merger.png +0 -0
  22. package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
  23. package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
  24. package/docs/images/schedule-merger-config.png +0 -0
  25. package/docs/images/schedule-merger-example-1.png +0 -0
  26. package/docs/images/vipps-plakat.png +0 -0
  27. package/docs/images/vipps-qr.png +0 -0
  28. package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
  29. package/docs/nodes/README.md +12 -6
  30. package/docs/nodes/dynamic-commands.md +79 -0
  31. package/docs/nodes/dynamic-config.md +76 -0
  32. package/docs/nodes/ps-elvia-add-tariff.md +4 -0
  33. package/docs/nodes/ps-general-add-tariff.md +10 -0
  34. package/docs/nodes/ps-receive-price.md +2 -1
  35. package/docs/nodes/ps-schedule-merger.md +227 -0
  36. package/docs/nodes/ps-strategy-best-save.md +46 -110
  37. package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
  38. package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
  39. package/docs/nodes/ps-strategy-lowest-price.md +51 -112
  40. package/package.json +5 -2
  41. package/src/elvia/elvia-add-tariff.html +1 -2
  42. package/src/elvia/elvia-add-tariff.js +1 -3
  43. package/src/elvia/elvia-api.js +9 -0
  44. package/src/elvia/elvia-tariff.html +1 -1
  45. package/src/general-add-tariff.html +14 -8
  46. package/src/general-add-tariff.js +0 -1
  47. package/src/handle-input.js +94 -106
  48. package/src/handle-output.js +109 -0
  49. package/src/receive-price-functions.js +3 -3
  50. package/src/schedule-merger-functions.js +98 -0
  51. package/src/schedule-merger.html +135 -0
  52. package/src/schedule-merger.js +108 -0
  53. package/src/strategy-best-save.html +38 -1
  54. package/src/strategy-best-save.js +17 -63
  55. package/src/strategy-fixed-schedule.html +339 -0
  56. package/src/strategy-fixed-schedule.js +84 -0
  57. package/src/strategy-functions.js +35 -0
  58. package/src/strategy-lowest-price.html +76 -38
  59. package/src/strategy-lowest-price.js +16 -35
  60. package/src/utils.js +75 -2
  61. package/test/commands-input-best-save.test.js +142 -0
  62. package/test/commands-input-lowest-price.test.js +149 -0
  63. package/test/commands-input-schedule-merger.test.js +128 -0
  64. package/test/data/best-save-overlap-result.json +5 -1
  65. package/test/data/best-save-result.json +4 -0
  66. package/test/data/commands-result-best-save.json +383 -0
  67. package/test/data/commands-result-lowest-price.json +340 -0
  68. package/test/data/fixed-schedule-result.json +353 -0
  69. package/test/data/lowest-price-result-cont-max-fail.json +5 -1
  70. package/test/data/lowest-price-result-cont-max.json +3 -1
  71. package/test/data/lowest-price-result-cont.json +8 -1
  72. package/test/data/lowest-price-result-missing-end.json +8 -3
  73. package/test/data/lowest-price-result-neg-cont.json +27 -0
  74. package/test/data/lowest-price-result-neg-split.json +23 -0
  75. package/test/data/lowest-price-result-split-allday.json +3 -1
  76. package/test/data/lowest-price-result-split-allday10.json +1 -0
  77. package/test/data/lowest-price-result-split-max.json +3 -1
  78. package/test/data/lowest-price-result-split.json +3 -1
  79. package/test/data/merge-schedule-data.js +238 -0
  80. package/test/data/negative-prices.json +197 -0
  81. package/test/data/nordpool-event-prices.json +96 -480
  82. package/test/data/nordpool-zero-prices.json +90 -0
  83. package/test/data/reconfigResult.js +1 -0
  84. package/test/data/result.js +1 -0
  85. package/test/data/tibber-result-end-0-24h.json +12 -2
  86. package/test/data/tibber-result-end-0.json +12 -2
  87. package/test/data/tibber-result.json +1 -0
  88. package/test/receive-price.test.js +22 -0
  89. package/test/schedule-merger-functions.test.js +101 -0
  90. package/test/schedule-merger-test-utils.js +27 -0
  91. package/test/schedule-merger.test.js +130 -0
  92. package/test/send-config-input.test.js +45 -2
  93. package/test/strategy-best-save-test-utils.js +1 -1
  94. package/test/strategy-best-save.test.js +45 -0
  95. package/test/strategy-fixed-schedule.test.js +117 -0
  96. package/test/strategy-heat-capacitor.test.js +1 -1
  97. package/test/strategy-lowest-price-functions.test.js +1 -1
  98. package/test/strategy-lowest-price-test-utils.js +31 -0
  99. package/test/strategy-lowest-price.test.js +55 -45
  100. package/test/test-utils.js +43 -36
  101. package/test/utils.test.js +13 -0
  102. package/docs/images/node-power-saver.png +0 -0
  103. package/docs/nodes/power-saver.md +0 -23
  104. package/src/power-saver.html +0 -116
  105. package/src/power-saver.js +0 -260
  106. package/test/commands-input.test.js +0 -47
  107. package/test/power-saver.test.js +0 -189
@@ -1,69 +1,253 @@
1
1
  # Capacity part of grid tariff
2
2
 
3
- I Norway, there has been introduced a monthly fee for grid capacity. The purpose is to get people to avoid using large capacities at any time.
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
- The capacity is divided in steps, so if your average of the 3 worst days is over the step limit, your fee is picked from that step.
5
+ ::: details A bug was found 12. sep 2022. Here is how to fix:
7
6
 
8
- Here is an example how this can be controlled. The axample contains several features, and yuo 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.
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
+ ![Capacity Flow](../images/example-capacity-flow.png)
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
- - Show status as Ok, Warning or Alarm for the current consumption (estimate calculated for the current hour).
25
- - Show current step, that is the step that you already are on based on consumption previously this month.
26
- - Control sensors in HA that you can use to take action to reduce consumption the current hour.
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 algorithm is calculating a state that can be:
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
- - Ok - Current estimate will not bring you over to another step.
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 current consumption is measured continuously (every 2 seconds), along with the total consumption for the current hour until current time.
37
- The consumption for the rest of the current hour is estimated, assumed to be the same in average as the average consumption for the last minute (can be configured to several minutes). Based on this, the total consumption for the current hour is estimated.
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 current hour is compared to the other hours today. If it is not the worst, status is Ok, as it will not affect the fee.
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. If today is ranked as 4 or better, where 1 is the worst this month, then status is Ok. It will not affect the fee.
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`. So, if the current hour is ranked as 3, and the `currentMonthlyEstimate` is still within `currentStep`, status is still Ok, as the current hour will not participate in braking the step limit.
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
- If today is ranked as 1, 2 or 3, and the `currentMonthlyEstimate` is over the `currentStep`, then the status is Alarm.
204
+ A boolean value, `true` if the status is `Alarm`, `false` if not.
48
205
 
49
- Else the status is Warning. That is if today is ranked as 1 or 2, but `currentMonthlyEstimate` is still under `currentStep`. In this case, the current hour makes the situation worse, and can potentially participate in breaking the limit together with a worse hour coming later this month.
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
- ## Output
209
+ ### Alarm Level
52
210
 
53
- The last step in the flow provides the following output:
211
+ A number from 0 to 9, meaning as follows:
54
212
 
55
- | Name | Description |
56
- | ---------------------- | ------------------------------------------------------------------------ |
57
- | status | The status of the current hour (Ok, Warning or Alarm). |
58
- | highestPerDay | An array with the worst hour for each day this month |
59
- | highestCounting | An array with the 3 worst days this month, including passed hours today. |
60
- | highestToday | The worst hour until now today, not including current hour. |
61
- | currentMonthlyEstimate | The average of the three worst days, including this hour. |
62
- | hourEstimate | The estimate for the current hour. |
63
- | currentStep | The upper limit of the step we are currently on. |
64
- | currentHourRanking | The rank of the current hour, from 0 to 4. See below. |
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
- ### currentHourRanking
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
- ### alarmLevel TODO
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
- | Value | Description |
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
- hourEstimate
97
- maxToday
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
- hourEstimate < maxToday
103
- hourEstimate > maxToday && hourEstimate < highestCounting[2]
104
- maxToday < hourEstimate < nextStep
105
- maxToday < hourEstimate < highestCounting[2]
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
- You can define the limits for `close` and `very close` to the next step, and the limit for `way over` the next step.
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
- If the current hour is
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. It sums up the actual consumption from the beginning of the hour until the current time, and adds the estimate for the rest of the hour, giving a total estimate for the 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. It uses the result and estimates the consumption for the rest of the hour, assuming that the consumption will be the same.
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 | Description |
200
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
201
- | accumulatedConsumption | Accumulated consumption the current day. |
202
- | periodSec | Period the average is calculated for, in seconds. It will increase in the beginning, until it reaches `ESTIMATION_TIME_MINUTES * 60` |
203
- | consumptionInPeriod | Consumption in the last `periodSec` seconds. Used as estimate for the remaining of the hour. |
204
- | timeLeftSec | Number of seconds left in the current hour. |
205
- | consumptionLeft | Estimated consumption the remaining of the hour. |
206
- | hourEstimate | The estimated consumption for the total hour. |
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
- ### Check for breach
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
+ ![Global context window](../images/global-context-window.png)
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
- This is where numbers are evaluated to figure out if the current hour is in risk of breaching the limit for the next step.
224
- The output is as follows:
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
- | Name | Description |
227
- | ---------------------- | ----------------------------------------------------------------------------------- |
228
- | status | The status of the current hour (Ok, Warning or Alarm). |
229
- | highestPerDay | An array with the worst hour for each day this month |
230
- | highestCounting | An array with the 3 worst days this month, including passed hours today. |
231
- | highestToday | The worst hour until now today, not including current hour. |
232
- | currentMonthlyEstimate | The average of the three worst days, including this hour. |
233
- | hourEstimate | The estimate for the current hour. |
234
- | currentStep | The upper limit of the step we are currently on. |
235
- | currentHourRanking | The rank of the current hour, from 1 to 4, where 4 is the lowest |
236
- | reductionRequired | The power that must be reduced the rest of the hour in order to not break the limit |
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
- ## Actions
1069
+ </CodeGroupItem>
1070
+ </CodeGroup>
239
1071
 
240
- You may set up actions to be taken when the status is Warning or Alarm.
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
- When a warning or an alarm has been on for a specified time, actions are taken.
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
- Actions are defined in an array in the beginning of the script.
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
- For each action, the following is specified:
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
- - sensor - The the entity id of the sensor that gives you the effect that will be reduced.
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
- The node has the same number of outputs as there are actions.
260
- The first output is for the first action, and so on.
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
- It is up to you to define how to use the actions.
1086
+ ```js
1087
+ const actions = flow.get("actions");
1088
+ const ha = global.get("homeassistant").homeAssistant;
266
1089
 
267
- # TO DO
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
- - Implement actions
270
- - Make a safety margin in kW so we reduce a little more than necessary.
271
- - May need some kind of warning when hourEstimate > currentStep, even if currentMonthlyEstimate is not.
272
- - Maybe set reductionRequired to hourEstimate - currentStep in this case.
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
+ :::