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
@@ -0,0 +1,1142 @@
1
+ [
2
+ {
3
+ "id": "2a073d402b1b6573",
4
+ "type": "function",
5
+ "z": "d938c47f.3398f8",
6
+ "name": "Build query for consumption",
7
+ "func": "/*\n Calculate number of hours to reeive consumption for,\n that is number of hours in the month until now.\n Constructs a tibber query to get consumption per hour.\n*/\n\nconst TIBBER_HOME_ID = \"put your tibber home id here\"\n\nconst timestamp = msg.payload.timestamp\n\n// Stop if hour has not changed\nconst time = new Date(timestamp)\nconst hour = time.getHours()\nconst previousHour = context.get(\"previousHour\")\nif(previousHour !== undefined && hour === previousHour) {\n return\n}\ncontext.set(\"previousHour\", hour)\n\n// Calculate number of hours to query\nconst date = time.getDate() - 1\nconst hour2 = time.getHours()\nconst count = date * 24 + hour2\n\n// Build query\nconst query = `\n{\n viewer {\n home (id: \"${TIBBER_HOME_ID}\") {\n consumption(resolution: HOURLY, last: ${count}) {\n nodes {\n from\n consumption\n }\n }\n }\n }\n}\n`\n\nmsg.payload = query\nreturn msg;",
8
+ "outputs": 1,
9
+ "noerr": 0,
10
+ "initialize": "// Code added here will be run once\n// whenever the node is started.\ncontext.set(\"previousHour\", undefined)",
11
+ "finalize": "",
12
+ "libs": [],
13
+ "x": 330,
14
+ "y": 970,
15
+ "wires": [["cf42844ec5bdfd21"]]
16
+ },
17
+ {
18
+ "id": "cf42844ec5bdfd21",
19
+ "type": "tibber-query",
20
+ "z": "d938c47f.3398f8",
21
+ "name": "Get consumption",
22
+ "active": true,
23
+ "apiEndpointRef": "b70ec5d0.6f8f08",
24
+ "x": 140,
25
+ "y": 1050,
26
+ "wires": [["172bdb20196bc56a", "a614116d19af0b02"]]
27
+ },
28
+ {
29
+ "id": "b5b84faebe49979e",
30
+ "type": "tibber-feed",
31
+ "z": "d938c47f.3398f8",
32
+ "name": "Get live data",
33
+ "active": true,
34
+ "apiEndpointRef": "b70ec5d0.6f8f08",
35
+ "homeId": "put your tibber home id here",
36
+ "timestamp": "1",
37
+ "power": "1",
38
+ "lastMeterConsumption": false,
39
+ "accumulatedConsumption": true,
40
+ "accumulatedProduction": false,
41
+ "accumulatedConsumptionLastHour": "1",
42
+ "accumulatedProductionLastHour": false,
43
+ "accumulatedCost": false,
44
+ "accumulatedReward": false,
45
+ "currency": false,
46
+ "minPower": false,
47
+ "averagePower": false,
48
+ "maxPower": false,
49
+ "powerProduction": false,
50
+ "minPowerProduction": false,
51
+ "maxPowerProduction": false,
52
+ "lastMeterProduction": false,
53
+ "powerFactor": false,
54
+ "voltagePhase1": false,
55
+ "voltagePhase2": false,
56
+ "voltagePhase3": false,
57
+ "currentL1": false,
58
+ "currentL2": false,
59
+ "currentL3": false,
60
+ "signalStrength": false,
61
+ "x": 100,
62
+ "y": 950,
63
+ "wires": [["90412687d7504168", "2a073d402b1b6573"]]
64
+ },
65
+ {
66
+ "id": "172bdb20196bc56a",
67
+ "type": "function",
68
+ "z": "d938c47f.3398f8",
69
+ "name": "Find highest per day",
70
+ "func": "const MAX_COUNTING = 3\nconst hours = msg.payload.viewer.home.consumption.nodes\nconst days = new Map()\nhours.forEach (h => {\n const date = (new Date(h.from)).getDate()\n if (!days.has(date) || h.consumption > days.get(date).consumption) {\n days.set(date, {from: h.from, consumption: h.consumption})\n }\n})\nconst highestToday = days.get((new Date()).getDate()) ?? {\n consumption: 0,\n from: null\n}\nconst highestPerDay = [...days.values()].sort((a, b) => b.consumption - a.consumption)\nconst highestCounting = highestPerDay.slice(0, MAX_COUNTING)\nconst currentMonthlyMaxAverage = highestCounting.length === 0 \n? 0 \n: highestCounting.reduce((prev, val) => \n prev + val.consumption, 0) / highestCounting.length\nmsg.payload = {\n highestPerDay,\n highestCounting,\n highestToday,\n currentMonthlyMaxAverage\n}\nreturn msg;",
71
+ "outputs": 1,
72
+ "noerr": 0,
73
+ "initialize": "",
74
+ "finalize": "",
75
+ "libs": [],
76
+ "x": 370,
77
+ "y": 1030,
78
+ "wires": [["deee9c5a2e504afd"]]
79
+ },
80
+ {
81
+ "id": "90412687d7504168",
82
+ "type": "function",
83
+ "z": "d938c47f.3398f8",
84
+ "name": "Collect estimate for hour",
85
+ "func": "\n// Number of minutes used to calculate assumed consumption:\nconst ESTIMATION_TIME_MINUTES = 1\n\nconst buffer = context.get(\"buffer\") || []\n\n// Add new record to buffer\nconst time = new Date(msg.payload.timestamp)\nconst timeMs = time.getTime()\nconst accumulatedConsumption = msg.payload.accumulatedConsumption\nconst accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour\nbuffer.push({timeMs, accumulatedConsumption})\n\nconst currentHour = new Date(msg.payload.timestamp)\ncurrentHour.setMinutes(0)\ncurrentHour.setSeconds(0)\n\n// Remove too old records from buffer\nconst maxAgeMs = ESTIMATION_TIME_MINUTES * 60 * 1000\nlet oldest = buffer[0]\nwhile ((timeMs - oldest.timeMs) > maxAgeMs) {\n buffer.splice(0, 1)\n oldest = buffer[0]\n}\ncontext.set(\"buffer\", buffer)\n\n// Calculate buffer\nconst periodMs = buffer[buffer.length - 1].timeMs - buffer[0].timeMs\nconst consumptionInPeriod = buffer[buffer.length - 1].accumulatedConsumption - buffer[0].accumulatedConsumption\nif (periodMs === 0) {\n return // First item in buffer\n}\n\n// Estimate remaining of current hour\nconst timeLeftMs = (60 * 60 * 1000) - (time.getMinutes() * 60000 + time.getSeconds() * 1000 + time.getMilliseconds())\nconst consumptionLeft = consumptionInPeriod / periodMs * timeLeftMs\nconst averageConsumptionNow = consumptionInPeriod / periodMs * 60 * 60 * 1000\n\n// Estimate total hour\nconst hourEstimate = accumulatedConsumptionLastHour + consumptionLeft + 0 // Change for testing\n\nmsg.payload = {\n accumulatedConsumption,\n accumulatedConsumptionLastHour,\n periodMs,\n consumptionInPeriod,\n averageConsumptionNow,\n timeLeftMs,\n consumptionLeft,\n hourEstimate,\n currentHour\n}\n\nreturn msg;",
86
+ "outputs": 1,
87
+ "noerr": 0,
88
+ "initialize": "// Code added here will be run once\n// whenever the node is started.\ncontext.set(\"buffer\", [])",
89
+ "finalize": "",
90
+ "libs": [],
91
+ "x": 320,
92
+ "y": 930,
93
+ "wires": [["deee9c5a2e504afd"]]
94
+ },
95
+ {
96
+ "id": "deee9c5a2e504afd",
97
+ "type": "function",
98
+ "z": "d938c47f.3398f8",
99
+ "name": "Calculate values",
100
+ "func": "const HA_NAME = \"homeAssistant\"; // Your HA name\nconst STEPS = [2, 5, 10, 15, 20]\nconst MAX_COUNTING = 3 // Number of days to calculate month\nconst BUFFER = 0.5 // Closer to limit increases level\nconst SAFE_ZONE = 2 // Further from limit reduces level\nconst ALARM = 8 // Min level that causes status to be alarm\n\nconst ha = global.get(\"homeassistant\")[HA_NAME];\nif(!ha.isConnected) {\n return\n}\n\nfunction isNull(value) {\n return value === null || value === undefined\n}\n\nfunction calculateLevel(hourEstimate, \n currentHourRanking,\n highestCountingAverageWithCurrent,\n nextStep) {\n if(currentHourRanking === 0) {\n return 0\n }\n if(highestCountingAverageWithCurrent > nextStep) {\n return 9\n }\n if(highestCountingAverageWithCurrent > (nextStep - BUFFER)) {\n return 8\n }\n if(hourEstimate > nextStep) {\n return 7\n }\n if(hourEstimate > (nextStep - BUFFER)) {\n return 6\n }\n if(currentHourRanking === 1 && (nextStep - hourEstimate) < SAFE_ZONE) {\n return 5\n }\n if(currentHourRanking === 2 && (nextStep - hourEstimate) < SAFE_ZONE) {\n return 4\n }\n if(currentHourRanking === 3 && (nextStep - hourEstimate) < SAFE_ZONE) {\n return 3\n }\n if(currentHourRanking === 1) {\n return 2\n }\n if(currentHourRanking === 2) {\n return 1\n }\n return 0\n}\n\n\nif (msg.payload.highestPerDay) {\n context.set(\"highestPerDay\", msg.payload.highestPerDay)\n context.set(\"highestCounting\", msg.payload.highestCounting)\n context.set(\"highestToday\", msg.payload.highestToday)\n context.set(\"currentMonthlyMaxAverage\", msg.payload.currentMonthlyMaxAverage)\n node.status({fill:\"green\",shape:\"ring\",text:\"Got ranking\"});\n return\n}\n\nconst highestPerDay = context.get(\"highestPerDay\")\nconst highestCounting = context.get(\"highestCounting\")\nconst highestToday = context.get(\"highestToday\")\nconst currentMonthlyMaxAverage = context.get(\"currentMonthlyMaxAverage\")\nconst hourEstimate = msg.payload.hourEstimate\nconst timeLeftMs = msg.payload.timeLeftMs\nconst timeLeftSec = timeLeftMs / 1000\nconst periodMs = msg.payload.periodMs\nconst accumulatedConsumption = msg.payload.accumulatedConsumption\nconst accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour\nconst consumptionLeft = msg.payload.consumptionLeft\nconst averageConsumptionNow = msg.payload.averageConsumptionNow\nconst currentHour = msg.payload.currentHour\n\nif (timeLeftSec === 0) {\n return null\n}\n\nif (isNull(highestPerDay)) {\n node.status({fill:\"red\",shape:\"dot\",text:\"No highest per day\"});\n return\n}\nif (isNull(highestToday)) {\n node.status({fill:\"red\",shape:\"dot\",text:\"No highest today\"});\n return\n}\nif (isNull(hourEstimate)) {\n node.status({fill:\"red\",shape:\"dot\",text:\"No estimate\"});\n return\n}\n\nconst currentStep = STEPS.reduceRight((prev, val) => val > currentMonthlyMaxAverage ? val : prev, STEPS[STEPS.length - 1])\n\n// Set currentHourRanking\nlet currentHourRanking = MAX_COUNTING + 1\nfor(let i = highestCounting.length - 1; i >= 0; i--) {\n if(hourEstimate > highestCounting[i].consumption) {\n currentHourRanking = i + 1\n }\n}\nif(hourEstimate < highestToday.consumption) {\n currentHourRanking = 0\n}\n\nconst current = {from: currentHour, consumption: hourEstimate}\nconst highestCountingWithCurrent = [...highestCounting, current].sort((a, b) => b.consumption - a.consumption).slice(0, highestCounting.length)\nconst currentMonthlyEstimate = highestCountingWithCurrent.length === 0 ? 0 : highestCountingWithCurrent.reduce((prev, val) => prev + val.consumption, 0) / highestCountingWithCurrent.length\n\n// Set alarm level\nconst alarmLevel = calculateLevel(\n hourEstimate,\n currentHourRanking,\n currentMonthlyEstimate,\n currentStep)\n\n// Evaluate status\nconst status = alarmLevel >= ALARM ? \"Alarm\" : alarmLevel > 0 ? \"Warning\" : \"Ok\"\n\n// Calculate reduction\nconst reductionRequired = alarmLevel < ALARM ? 0 :\n Math.max((currentMonthlyEstimate - currentStep) * highestCounting.length, 0)\n * 3600 / timeLeftSec;\nconst reductionRecommended = alarmLevel < 3 ? 0 :\n Math.max(hourEstimate + SAFE_ZONE - currentStep, 0)\n * 3600 / timeLeftSec;\n\n// Calculate increase possible\nconst increasePossible = alarmLevel >= 3 ? 0 :\n Math.max(currentStep - hourEstimate - SAFE_ZONE, 0)\n * 3600 / timeLeftSec;\n\n// Create output\nconst fill = status === \"Ok\" ? \"green\" : status === \"Alarm\" ? \"red\" : \"yellow\";\nnode.status({fill,shape:\"dot\",text:\"Working\"});\n\nconst RESOLUTION = 1000\n\nconst payload = {\n status, // Ok, Warning, Alarm\n statusOk: status === \"Ok\",\n statusWarning: status === \"Warning\",\n statusAlarm: status === \"Alarm\",\n alarmLevel,\n highestPerDay,\n highestCounting,\n highestCountingWithCurrent,\n highestToday,\n currentMonthlyEstimate: Math.round(currentMonthlyEstimate * RESOLUTION) / RESOLUTION,\n accumulatedConsumptionLastHour: Math.round(accumulatedConsumptionLastHour * RESOLUTION) / RESOLUTION,\n consumptionLeft: Math.round(consumptionLeft * RESOLUTION) / RESOLUTION,\n hourEstimate: Math.round(hourEstimate * RESOLUTION) / RESOLUTION,\n averageConsumptionNow: Math.round(averageConsumptionNow * RESOLUTION) / RESOLUTION,\n reductionRequired: Math.round(reductionRequired * RESOLUTION) / RESOLUTION,\n reductionRecommended: Math.round(reductionRecommended * RESOLUTION) / RESOLUTION,\n increasePossible: Math.round(increasePossible * RESOLUTION) / RESOLUTION,\n currentStep,\n currentHourRanking,\n timeLeftSec,\n periodMs,\n accumulatedConsumption\n}\n\nmsg.payload = payload\n\nreturn msg;",
101
+ "outputs": 1,
102
+ "noerr": 0,
103
+ "initialize": "",
104
+ "finalize": "",
105
+ "libs": [],
106
+ "x": 580,
107
+ "y": 930,
108
+ "wires": [
109
+ [
110
+ "3f26ca0dae54f120",
111
+ "a9d94489cb429ac1",
112
+ "b7a03b817ed5481e",
113
+ "dbc3786eebd34825",
114
+ "cd77d1f6acdcebde",
115
+ "0fb7cffd909675d4",
116
+ "949b11e8d1d05398",
117
+ "e0a57b0ae1ef8424",
118
+ "bcece06057fe66ef",
119
+ "72886b57ce2fcd4e",
120
+ "e4e1dfad359aa8b8",
121
+ "291a07c14f9d2dbf",
122
+ "05e693dab4f13d93",
123
+ "15b8bd222daca026",
124
+ "241d0b3a591a26b2",
125
+ "7b9668ca57ce9fef",
126
+ "ea38e7cbf080f15f",
127
+ "51c0696c32cc1822",
128
+ "ac0b86c136f40790",
129
+ "3cdb68064ac5a5bc"
130
+ ]
131
+ ]
132
+ },
133
+ {
134
+ "id": "3f26ca0dae54f120",
135
+ "type": "ha-entity",
136
+ "z": "d938c47f.3398f8",
137
+ "name": "Capacity Status",
138
+ "server": "ec4a12a1.b2be9",
139
+ "version": 2,
140
+ "debugenabled": false,
141
+ "outputs": 1,
142
+ "entityType": "sensor",
143
+ "config": [
144
+ {
145
+ "property": "name",
146
+ "value": "PS Cap Status"
147
+ },
148
+ {
149
+ "property": "device_class",
150
+ "value": ""
151
+ },
152
+ {
153
+ "property": "icon",
154
+ "value": ""
155
+ },
156
+ {
157
+ "property": "unit_of_measurement",
158
+ "value": ""
159
+ },
160
+ {
161
+ "property": "state_class",
162
+ "value": ""
163
+ },
164
+ {
165
+ "property": "last_reset",
166
+ "value": ""
167
+ }
168
+ ],
169
+ "state": "payload.status",
170
+ "stateType": "msg",
171
+ "attributes": [],
172
+ "resend": true,
173
+ "outputLocation": "payload",
174
+ "outputLocationType": "none",
175
+ "inputOverride": "allow",
176
+ "outputOnStateChange": false,
177
+ "outputPayload": "",
178
+ "outputPayloadType": "str",
179
+ "x": 780,
180
+ "y": 990,
181
+ "wires": [[]]
182
+ },
183
+ {
184
+ "id": "a9d94489cb429ac1",
185
+ "type": "ha-entity",
186
+ "z": "d938c47f.3398f8",
187
+ "name": "Capacity Warning",
188
+ "server": "ec4a12a1.b2be9",
189
+ "version": 2,
190
+ "debugenabled": false,
191
+ "outputs": 1,
192
+ "entityType": "binary_sensor",
193
+ "config": [
194
+ {
195
+ "property": "name",
196
+ "value": "PS Cap Warning"
197
+ },
198
+ {
199
+ "property": "device_class",
200
+ "value": "none"
201
+ },
202
+ {
203
+ "property": "icon",
204
+ "value": ""
205
+ },
206
+ {
207
+ "property": "unit_of_measurement",
208
+ "value": ""
209
+ },
210
+ {
211
+ "property": "state_class",
212
+ "value": ""
213
+ },
214
+ {
215
+ "property": "last_reset",
216
+ "value": ""
217
+ }
218
+ ],
219
+ "state": "payload.statusWarning",
220
+ "stateType": "msg",
221
+ "attributes": [],
222
+ "resend": true,
223
+ "outputLocation": "payload",
224
+ "outputLocationType": "none",
225
+ "inputOverride": "allow",
226
+ "outputOnStateChange": false,
227
+ "outputPayload": "",
228
+ "outputPayloadType": "str",
229
+ "x": 790,
230
+ "y": 1090,
231
+ "wires": [[]]
232
+ },
233
+ {
234
+ "id": "b7a03b817ed5481e",
235
+ "type": "ha-entity",
236
+ "z": "d938c47f.3398f8",
237
+ "name": "Capacity Alarm",
238
+ "server": "ec4a12a1.b2be9",
239
+ "version": 2,
240
+ "debugenabled": false,
241
+ "outputs": 1,
242
+ "entityType": "binary_sensor",
243
+ "config": [
244
+ {
245
+ "property": "name",
246
+ "value": "PS Cap Alarm"
247
+ },
248
+ {
249
+ "property": "device_class",
250
+ "value": "none"
251
+ },
252
+ {
253
+ "property": "icon",
254
+ "value": ""
255
+ },
256
+ {
257
+ "property": "unit_of_measurement",
258
+ "value": ""
259
+ },
260
+ {
261
+ "property": "state_class",
262
+ "value": ""
263
+ },
264
+ {
265
+ "property": "last_reset",
266
+ "value": ""
267
+ }
268
+ ],
269
+ "state": "payload.statusAlarm",
270
+ "stateType": "msg",
271
+ "attributes": [],
272
+ "resend": true,
273
+ "outputLocation": "payload",
274
+ "outputLocationType": "none",
275
+ "inputOverride": "allow",
276
+ "outputOnStateChange": false,
277
+ "outputPayload": "",
278
+ "outputPayloadType": "str",
279
+ "x": 780,
280
+ "y": 1140,
281
+ "wires": [[]]
282
+ },
283
+ {
284
+ "id": "dbc3786eebd34825",
285
+ "type": "ha-entity",
286
+ "z": "d938c47f.3398f8",
287
+ "name": "Capacity Ok",
288
+ "server": "ec4a12a1.b2be9",
289
+ "version": 2,
290
+ "debugenabled": false,
291
+ "outputs": 1,
292
+ "entityType": "binary_sensor",
293
+ "config": [
294
+ {
295
+ "property": "name",
296
+ "value": "PS Cap Ok"
297
+ },
298
+ {
299
+ "property": "device_class",
300
+ "value": "none"
301
+ },
302
+ {
303
+ "property": "icon",
304
+ "value": ""
305
+ },
306
+ {
307
+ "property": "unit_of_measurement",
308
+ "value": ""
309
+ },
310
+ {
311
+ "property": "state_class",
312
+ "value": ""
313
+ },
314
+ {
315
+ "property": "last_reset",
316
+ "value": ""
317
+ }
318
+ ],
319
+ "state": "payload.statusOk",
320
+ "stateType": "msg",
321
+ "attributes": [],
322
+ "resend": true,
323
+ "outputLocation": "payload",
324
+ "outputLocationType": "none",
325
+ "inputOverride": "allow",
326
+ "outputOnStateChange": false,
327
+ "outputPayload": "",
328
+ "outputPayloadType": "str",
329
+ "x": 770,
330
+ "y": 1040,
331
+ "wires": [[]]
332
+ },
333
+ {
334
+ "id": "cd77d1f6acdcebde",
335
+ "type": "ha-entity",
336
+ "z": "d938c47f.3398f8",
337
+ "name": "Capacity Current Step",
338
+ "server": "ec4a12a1.b2be9",
339
+ "version": 2,
340
+ "debugenabled": false,
341
+ "outputs": 1,
342
+ "entityType": "sensor",
343
+ "config": [
344
+ {
345
+ "property": "name",
346
+ "value": "PS Cap Current Step"
347
+ },
348
+ {
349
+ "property": "device_class",
350
+ "value": "power"
351
+ },
352
+ {
353
+ "property": "icon",
354
+ "value": ""
355
+ },
356
+ {
357
+ "property": "unit_of_measurement",
358
+ "value": "kW"
359
+ },
360
+ {
361
+ "property": "state_class",
362
+ "value": ""
363
+ },
364
+ {
365
+ "property": "last_reset",
366
+ "value": ""
367
+ }
368
+ ],
369
+ "state": "payload.currentStep",
370
+ "stateType": "msg",
371
+ "attributes": [],
372
+ "resend": true,
373
+ "outputLocation": "payload",
374
+ "outputLocationType": "none",
375
+ "inputOverride": "allow",
376
+ "outputOnStateChange": false,
377
+ "outputPayload": "",
378
+ "outputPayloadType": "str",
379
+ "x": 800,
380
+ "y": 1240,
381
+ "wires": [[]]
382
+ },
383
+ {
384
+ "id": "0fb7cffd909675d4",
385
+ "type": "ha-entity",
386
+ "z": "d938c47f.3398f8",
387
+ "name": "Capacity Hour Estimate",
388
+ "server": "ec4a12a1.b2be9",
389
+ "version": 2,
390
+ "debugenabled": false,
391
+ "outputs": 1,
392
+ "entityType": "sensor",
393
+ "config": [
394
+ {
395
+ "property": "name",
396
+ "value": "PS Cap Hour Estimate"
397
+ },
398
+ {
399
+ "property": "device_class",
400
+ "value": "power"
401
+ },
402
+ {
403
+ "property": "icon",
404
+ "value": ""
405
+ },
406
+ {
407
+ "property": "unit_of_measurement",
408
+ "value": "kW"
409
+ },
410
+ {
411
+ "property": "state_class",
412
+ "value": ""
413
+ },
414
+ {
415
+ "property": "last_reset",
416
+ "value": ""
417
+ }
418
+ ],
419
+ "state": "payload.hourEstimate",
420
+ "stateType": "msg",
421
+ "attributes": [],
422
+ "resend": true,
423
+ "outputLocation": "payload",
424
+ "outputLocationType": "none",
425
+ "inputOverride": "allow",
426
+ "outputOnStateChange": false,
427
+ "outputPayload": "",
428
+ "outputPayloadType": "str",
429
+ "x": 810,
430
+ "y": 1290,
431
+ "wires": [[]]
432
+ },
433
+ {
434
+ "id": "949b11e8d1d05398",
435
+ "type": "ha-entity",
436
+ "z": "d938c47f.3398f8",
437
+ "name": "Capacity Current Hour Ranking",
438
+ "server": "ec4a12a1.b2be9",
439
+ "version": 2,
440
+ "debugenabled": false,
441
+ "outputs": 1,
442
+ "entityType": "sensor",
443
+ "config": [
444
+ {
445
+ "property": "name",
446
+ "value": "PS Cap Current Hour Ranking"
447
+ },
448
+ {
449
+ "property": "device_class",
450
+ "value": ""
451
+ },
452
+ {
453
+ "property": "icon",
454
+ "value": ""
455
+ },
456
+ {
457
+ "property": "unit_of_measurement",
458
+ "value": ""
459
+ },
460
+ {
461
+ "property": "state_class",
462
+ "value": ""
463
+ },
464
+ {
465
+ "property": "last_reset",
466
+ "value": ""
467
+ }
468
+ ],
469
+ "state": "payload.currentHourRanking",
470
+ "stateType": "msg",
471
+ "attributes": [],
472
+ "resend": true,
473
+ "outputLocation": "payload",
474
+ "outputLocationType": "none",
475
+ "inputOverride": "allow",
476
+ "outputOnStateChange": false,
477
+ "outputPayload": "",
478
+ "outputPayloadType": "str",
479
+ "x": 830,
480
+ "y": 1340,
481
+ "wires": [[]]
482
+ },
483
+ {
484
+ "id": "e0a57b0ae1ef8424",
485
+ "type": "ha-entity",
486
+ "z": "d938c47f.3398f8",
487
+ "name": "Capacity Monthly Estimate",
488
+ "server": "ec4a12a1.b2be9",
489
+ "version": 2,
490
+ "debugenabled": false,
491
+ "outputs": 1,
492
+ "entityType": "sensor",
493
+ "config": [
494
+ {
495
+ "property": "name",
496
+ "value": "PS Cap Monthly Estimate"
497
+ },
498
+ {
499
+ "property": "device_class",
500
+ "value": "power"
501
+ },
502
+ {
503
+ "property": "icon",
504
+ "value": ""
505
+ },
506
+ {
507
+ "property": "unit_of_measurement",
508
+ "value": "kW"
509
+ },
510
+ {
511
+ "property": "state_class",
512
+ "value": ""
513
+ },
514
+ {
515
+ "property": "last_reset",
516
+ "value": ""
517
+ }
518
+ ],
519
+ "state": "payload.currentMonthlyEstimate",
520
+ "stateType": "msg",
521
+ "attributes": [],
522
+ "resend": true,
523
+ "outputLocation": "payload",
524
+ "outputLocationType": "none",
525
+ "inputOverride": "allow",
526
+ "outputOnStateChange": false,
527
+ "outputPayload": "",
528
+ "outputPayloadType": "str",
529
+ "x": 820,
530
+ "y": 1390,
531
+ "wires": [[]]
532
+ },
533
+ {
534
+ "id": "bcece06057fe66ef",
535
+ "type": "ha-entity",
536
+ "z": "d938c47f.3398f8",
537
+ "name": "Capacity Highest Today",
538
+ "server": "ec4a12a1.b2be9",
539
+ "version": 2,
540
+ "debugenabled": false,
541
+ "outputs": 1,
542
+ "entityType": "sensor",
543
+ "config": [
544
+ {
545
+ "property": "name",
546
+ "value": "PS Cap Highest Today"
547
+ },
548
+ {
549
+ "property": "device_class",
550
+ "value": "power"
551
+ },
552
+ {
553
+ "property": "icon",
554
+ "value": ""
555
+ },
556
+ {
557
+ "property": "unit_of_measurement",
558
+ "value": "kW"
559
+ },
560
+ {
561
+ "property": "state_class",
562
+ "value": ""
563
+ },
564
+ {
565
+ "property": "last_reset",
566
+ "value": ""
567
+ }
568
+ ],
569
+ "state": "payload.highestToday.consumption",
570
+ "stateType": "msg",
571
+ "attributes": [],
572
+ "resend": true,
573
+ "outputLocation": "payload",
574
+ "outputLocationType": "none",
575
+ "inputOverride": "allow",
576
+ "outputOnStateChange": false,
577
+ "outputPayload": "",
578
+ "outputPayloadType": "str",
579
+ "x": 1090,
580
+ "y": 970,
581
+ "wires": [[]]
582
+ },
583
+ {
584
+ "id": "72886b57ce2fcd4e",
585
+ "type": "ha-entity",
586
+ "z": "d938c47f.3398f8",
587
+ "name": "Capacity Highest Today Hour",
588
+ "server": "ec4a12a1.b2be9",
589
+ "version": 2,
590
+ "debugenabled": false,
591
+ "outputs": 1,
592
+ "entityType": "sensor",
593
+ "config": [
594
+ {
595
+ "property": "name",
596
+ "value": "PS Cap Highest Today Hour"
597
+ },
598
+ {
599
+ "property": "device_class",
600
+ "value": "timestamp"
601
+ },
602
+ {
603
+ "property": "icon",
604
+ "value": ""
605
+ },
606
+ {
607
+ "property": "unit_of_measurement",
608
+ "value": ""
609
+ },
610
+ {
611
+ "property": "state_class",
612
+ "value": ""
613
+ },
614
+ {
615
+ "property": "last_reset",
616
+ "value": ""
617
+ }
618
+ ],
619
+ "state": "payload.highestToday.from",
620
+ "stateType": "msg",
621
+ "attributes": [],
622
+ "resend": true,
623
+ "outputLocation": "payload",
624
+ "outputLocationType": "none",
625
+ "inputOverride": "allow",
626
+ "outputOnStateChange": false,
627
+ "outputPayload": "",
628
+ "outputPayloadType": "str",
629
+ "x": 1100,
630
+ "y": 1020,
631
+ "wires": [[]]
632
+ },
633
+ {
634
+ "id": "e4e1dfad359aa8b8",
635
+ "type": "ha-entity",
636
+ "z": "d938c47f.3398f8",
637
+ "name": "Capacity Reduction Required",
638
+ "server": "ec4a12a1.b2be9",
639
+ "version": 2,
640
+ "debugenabled": false,
641
+ "outputs": 1,
642
+ "entityType": "sensor",
643
+ "config": [
644
+ {
645
+ "property": "name",
646
+ "value": "PS Cap Reduction Required"
647
+ },
648
+ {
649
+ "property": "device_class",
650
+ "value": "power"
651
+ },
652
+ {
653
+ "property": "icon",
654
+ "value": ""
655
+ },
656
+ {
657
+ "property": "unit_of_measurement",
658
+ "value": "kW"
659
+ },
660
+ {
661
+ "property": "state_class",
662
+ "value": ""
663
+ },
664
+ {
665
+ "property": "last_reset",
666
+ "value": ""
667
+ }
668
+ ],
669
+ "state": "payload.reductionRequired",
670
+ "stateType": "msg",
671
+ "attributes": [],
672
+ "resend": true,
673
+ "outputLocation": "payload",
674
+ "outputLocationType": "none",
675
+ "inputOverride": "allow",
676
+ "outputOnStateChange": false,
677
+ "outputPayload": "",
678
+ "outputPayloadType": "str",
679
+ "x": 1110,
680
+ "y": 1070,
681
+ "wires": [[]]
682
+ },
683
+ {
684
+ "id": "291a07c14f9d2dbf",
685
+ "type": "ha-entity",
686
+ "z": "d938c47f.3398f8",
687
+ "name": "Capacity Estimate Rest Of Hour",
688
+ "server": "ec4a12a1.b2be9",
689
+ "version": 2,
690
+ "debugenabled": false,
691
+ "outputs": 1,
692
+ "entityType": "sensor",
693
+ "config": [
694
+ {
695
+ "property": "name",
696
+ "value": "PS Cap Estimate Rest Of Hour"
697
+ },
698
+ {
699
+ "property": "device_class",
700
+ "value": "power"
701
+ },
702
+ {
703
+ "property": "icon",
704
+ "value": ""
705
+ },
706
+ {
707
+ "property": "unit_of_measurement",
708
+ "value": "kW"
709
+ },
710
+ {
711
+ "property": "state_class",
712
+ "value": ""
713
+ },
714
+ {
715
+ "property": "last_reset",
716
+ "value": ""
717
+ }
718
+ ],
719
+ "state": "payload.consumptionLeft",
720
+ "stateType": "msg",
721
+ "attributes": [],
722
+ "resend": true,
723
+ "outputLocation": "payload",
724
+ "outputLocationType": "none",
725
+ "inputOverride": "allow",
726
+ "outputOnStateChange": false,
727
+ "outputPayload": "",
728
+ "outputPayloadType": "str",
729
+ "x": 1110,
730
+ "y": 1230,
731
+ "wires": [[]]
732
+ },
733
+ {
734
+ "id": "05e693dab4f13d93",
735
+ "type": "ha-entity",
736
+ "z": "d938c47f.3398f8",
737
+ "name": "Capacity Consumption Accumulated Hour",
738
+ "server": "ec4a12a1.b2be9",
739
+ "version": 2,
740
+ "debugenabled": false,
741
+ "outputs": 1,
742
+ "entityType": "sensor",
743
+ "config": [
744
+ {
745
+ "property": "name",
746
+ "value": "PS Cap Consumption Accumulated Hour"
747
+ },
748
+ {
749
+ "property": "device_class",
750
+ "value": "power"
751
+ },
752
+ {
753
+ "property": "icon",
754
+ "value": ""
755
+ },
756
+ {
757
+ "property": "unit_of_measurement",
758
+ "value": "kW"
759
+ },
760
+ {
761
+ "property": "state_class",
762
+ "value": ""
763
+ },
764
+ {
765
+ "property": "last_reset",
766
+ "value": ""
767
+ }
768
+ ],
769
+ "state": "payload.accumulatedConsumptionLastHour",
770
+ "stateType": "msg",
771
+ "attributes": [],
772
+ "resend": true,
773
+ "outputLocation": "payload",
774
+ "outputLocationType": "none",
775
+ "inputOverride": "allow",
776
+ "outputOnStateChange": false,
777
+ "outputPayload": "",
778
+ "outputPayloadType": "str",
779
+ "x": 1130,
780
+ "y": 1280,
781
+ "wires": [[]]
782
+ },
783
+ {
784
+ "id": "15b8bd222daca026",
785
+ "type": "ha-entity",
786
+ "z": "d938c47f.3398f8",
787
+ "name": "Capacity Time Left",
788
+ "server": "ec4a12a1.b2be9",
789
+ "version": 2,
790
+ "debugenabled": false,
791
+ "outputs": 1,
792
+ "entityType": "sensor",
793
+ "config": [
794
+ {
795
+ "property": "name",
796
+ "value": "PS Cap Time Left"
797
+ },
798
+ {
799
+ "property": "device_class",
800
+ "value": "time"
801
+ },
802
+ {
803
+ "property": "icon",
804
+ "value": ""
805
+ },
806
+ {
807
+ "property": "unit_of_measurement",
808
+ "value": "sec"
809
+ },
810
+ {
811
+ "property": "state_class",
812
+ "value": ""
813
+ },
814
+ {
815
+ "property": "last_reset",
816
+ "value": ""
817
+ }
818
+ ],
819
+ "state": "payload.timeLeftSec",
820
+ "stateType": "msg",
821
+ "attributes": [],
822
+ "resend": true,
823
+ "outputLocation": "payload",
824
+ "outputLocationType": "none",
825
+ "inputOverride": "allow",
826
+ "outputOnStateChange": false,
827
+ "outputPayload": "",
828
+ "outputPayloadType": "str",
829
+ "x": 1070,
830
+ "y": 1330,
831
+ "wires": [[]]
832
+ },
833
+ {
834
+ "id": "241d0b3a591a26b2",
835
+ "type": "ha-entity",
836
+ "z": "d938c47f.3398f8",
837
+ "name": "Capacity Consumption Now",
838
+ "server": "ec4a12a1.b2be9",
839
+ "version": 2,
840
+ "debugenabled": false,
841
+ "outputs": 1,
842
+ "entityType": "sensor",
843
+ "config": [
844
+ {
845
+ "property": "name",
846
+ "value": "PS Cap Consumption Now"
847
+ },
848
+ {
849
+ "property": "device_class",
850
+ "value": "power"
851
+ },
852
+ {
853
+ "property": "icon",
854
+ "value": ""
855
+ },
856
+ {
857
+ "property": "unit_of_measurement",
858
+ "value": "kW"
859
+ },
860
+ {
861
+ "property": "state_class",
862
+ "value": ""
863
+ },
864
+ {
865
+ "property": "last_reset",
866
+ "value": ""
867
+ }
868
+ ],
869
+ "state": "payload.averageConsumptionNow",
870
+ "stateType": "msg",
871
+ "attributes": [],
872
+ "resend": true,
873
+ "outputLocation": "payload",
874
+ "outputLocationType": "none",
875
+ "inputOverride": "allow",
876
+ "outputOnStateChange": false,
877
+ "outputPayload": "",
878
+ "outputPayloadType": "str",
879
+ "x": 1100,
880
+ "y": 1390,
881
+ "wires": [[]]
882
+ },
883
+ {
884
+ "id": "3cdb68064ac5a5bc",
885
+ "type": "function",
886
+ "z": "d938c47f.3398f8",
887
+ "name": "Reduction Actions",
888
+ "func": "const MIN_CONSUMPTION_TO_CARE = 0.05 // Do not reduce unless at least 50W\n\nconst actions = flow.get(\"actions\")\nconst ha = global.get(\"homeassistant\").homeAssistant\n\nlet reductionRequired = msg.payload.reductionRequired\nlet reductionRecommended = msg.payload.reductionRecommended\n\nif(reductionRecommended <= 0) {\n return null\n}\n\nfunction takeAction(action, consumption ) {\n const info = {\n time: new Date().toISOString(),\n name: \"Reduction action\",\n data: msg.payload,\n action\n }\n node.send([{ payload: action.payloadToTakeAction }, {payload: info}])\n reductionRequired = Math.max(0, reductionRequired - consumption)\n reductionRecommended = Math.max(0, reductionRecommended - consumption)\n action.actionTaken = true\n action.actionTime = Date.now()\n action.savedConsumption = consumption\n flow.set(\"actions\", actions)\n}\n\nfunction getConsumption(consumption) {\n if(typeof consumption === \"string\") {\n const sensor = ha.states[consumption]\n return sensor.state \n } else if (typeof consumption === \"number\") {\n return consumption\n } else if(typeof consumption === \"function\") {\n return consumption()\n } else {\n node.warn(\"Config error: consumption has illegal type: \" + typeof consumption)\n return 0\n }\n}\n\nactions\n.filter(a => msg.payload.alarmLevel >= a.minAlarmLevel && !a.actionTaken)\n.forEach(a => {\n const consumption = getConsumption(a.consumption)\n if (consumption < MIN_CONSUMPTION_TO_CARE) {\n return\n }\n if (reductionRequired > 0 || (reductionRecommended > 0 && a.reduceWhenRecommended)) {\n takeAction(a, consumption)\n }\n})\n \n",
889
+ "outputs": 2,
890
+ "noerr": 0,
891
+ "initialize": "// You MUST edit the actions array with your own actions.\n\nconst actions = [\n { \n consumption: \"sensor.varmtvannsbereder_electric_consumption_w\",\n name: \"Varmtvannsbereder\",\n id: \"vvb\",\n minAlarmLevel: 3,\n reduceWhenRecommended: true,\n minTimeOffSec: 300,\n payloadToTakeAction: {\n domain: \"switch\",\n service: \"turn_off\",\n target: {\n entity_id: [\"switch.varmtvannsbereder\"]\n }\n },\n payloadToResetAction: {\n domain: \"switch\",\n service: \"turn_on\",\n target: {\n entity_id: [\"switch.varmtvannsbereder\"]\n }\n }\n },\n { \n consumption: \"sensor.varme_gulv_bad_electric_consumption_w_2\",\n name: \"Varme gulv bad 1. etg.\",\n id: \"gulvbad\",\n minAlarmLevel: 3,\n reduceWhenRecommended: true,\n minTimeOffSec: 300,\n payloadToTakeAction: {\n domain: \"climate\",\n service: \"turn_off\",\n target: {\n entity_id: [\"climate.varme_gulv_bad_2\"]\n }\n },\n payloadToResetAction: {\n domain: \"climate\",\n service: \"turn_on\",\n target: {\n entity_id: [\"climate.varme_gulv_bad_2\"]\n }\n }\n },\n { \n consumption: \"sensor.varme_gulv_gang_electric_consumption_w\",\n name: \"Varme gulv gang 1. etg.\",\n id: \"gulvgang\",\n minAlarmLevel: 3,\n reduceWhenRecommended: true,\n minTimeOffSec: 300,\n payloadToTakeAction: {\n domain: \"climate\",\n service: \"turn_off\",\n target: {\n entity_id: [\"climate.varme_gulv_gang\"]\n }\n },\n payloadToResetAction: {\n domain: \"climate\",\n service: \"turn_on\",\n target: {\n entity_id: [\"climate.varme_gulv_gang\"]\n }\n }\n },\n {\n consumption: \"sensor.varme_gulv_kjellerstue_electric_consumption_w\",\n name: \"Varme gulv kjellerstue\",\n id: \"gulvkjeller\",\n minAlarmLevel: 3,\n reduceWhenRecommended: true,\n minTimeOffSec: 300,\n payloadToTakeAction: {\n domain: \"climate\",\n service: \"turn_off\",\n target: {\n entity_id: [\"climate.varme_gulv_kjellerstue\"]\n }\n },\n payloadToResetAction: {\n domain: \"climate\",\n service: \"turn_on\",\n target: {\n entity_id: [\"climate.varme_gulv_kjellerstue\"]\n }\n }\n },\n {\n consumption: 0.1,\n name: \"Test\",\n id: \"test\",\n minAlarmLevel: 3,\n reduceWhenRecommended: true,\n minTimeOffSec: 30,\n payloadToTakeAction: {\n domain: \"switch\",\n service: \"turn_off\",\n target: {\n entity_id: [\"switch.lys_kjokkenskap_switch\"]\n }\n },\n payloadToResetAction: {\n domain: \"switch\",\n service: \"turn_on\",\n target: {\n entity_id: [\"switch.lys_kjokkenskap_switch\"]\n }\n }\n }\n]\n// End of actions array\n\n// DO NOT DELETE THE CODE BELOW\n\n// Set default values for all actions\nactions.forEach(a => {\n a.actionTaken = false\n a.savedConsumption = 0\n})\n\nflow.set(\"actions\", actions)\n",
892
+ "finalize": "const actions = flow.get(\"actions\")\n\nactions\n .filter(a => a.actionTaken)\n .forEach(a => \n node.send({ payload: a.payloadToResetAction })\n )",
893
+ "libs": [],
894
+ "x": 250,
895
+ "y": 1210,
896
+ "wires": [["28a20e58f1058b6d"], ["1d738e15969dd163"]]
897
+ },
898
+ {
899
+ "id": "7b9668ca57ce9fef",
900
+ "type": "ha-entity",
901
+ "z": "d938c47f.3398f8",
902
+ "name": "Capacity Alarm Level",
903
+ "server": "ec4a12a1.b2be9",
904
+ "version": 2,
905
+ "debugenabled": false,
906
+ "outputs": 1,
907
+ "entityType": "sensor",
908
+ "config": [
909
+ {
910
+ "property": "name",
911
+ "value": "PS Cap Alarm Level"
912
+ },
913
+ {
914
+ "property": "device_class",
915
+ "value": ""
916
+ },
917
+ {
918
+ "property": "icon",
919
+ "value": ""
920
+ },
921
+ {
922
+ "property": "unit_of_measurement",
923
+ "value": ""
924
+ },
925
+ {
926
+ "property": "state_class",
927
+ "value": ""
928
+ },
929
+ {
930
+ "property": "last_reset",
931
+ "value": ""
932
+ }
933
+ ],
934
+ "state": "payload.alarmLevel",
935
+ "stateType": "msg",
936
+ "attributes": [],
937
+ "resend": true,
938
+ "outputLocation": "payload",
939
+ "outputLocationType": "none",
940
+ "inputOverride": "allow",
941
+ "outputOnStateChange": false,
942
+ "outputPayload": "",
943
+ "outputPayloadType": "str",
944
+ "x": 800,
945
+ "y": 1190,
946
+ "wires": [[]]
947
+ },
948
+ {
949
+ "id": "ea38e7cbf080f15f",
950
+ "type": "ha-entity",
951
+ "z": "d938c47f.3398f8",
952
+ "name": "Capacity Reduction Recommended",
953
+ "server": "ec4a12a1.b2be9",
954
+ "version": 2,
955
+ "debugenabled": false,
956
+ "outputs": 1,
957
+ "entityType": "sensor",
958
+ "config": [
959
+ {
960
+ "property": "name",
961
+ "value": "PS Cap Reduction Recommended"
962
+ },
963
+ {
964
+ "property": "device_class",
965
+ "value": "power"
966
+ },
967
+ {
968
+ "property": "icon",
969
+ "value": ""
970
+ },
971
+ {
972
+ "property": "unit_of_measurement",
973
+ "value": "kW"
974
+ },
975
+ {
976
+ "property": "state_class",
977
+ "value": ""
978
+ },
979
+ {
980
+ "property": "last_reset",
981
+ "value": ""
982
+ }
983
+ ],
984
+ "state": "payload.reductionRecommended",
985
+ "stateType": "msg",
986
+ "attributes": [],
987
+ "resend": true,
988
+ "outputLocation": "payload",
989
+ "outputLocationType": "none",
990
+ "inputOverride": "allow",
991
+ "outputOnStateChange": false,
992
+ "outputPayload": "",
993
+ "outputPayloadType": "str",
994
+ "x": 1100,
995
+ "y": 1120,
996
+ "wires": [[]]
997
+ },
998
+ {
999
+ "id": "51c0696c32cc1822",
1000
+ "type": "ha-entity",
1001
+ "z": "d938c47f.3398f8",
1002
+ "name": "Capacity Increase Possible",
1003
+ "server": "ec4a12a1.b2be9",
1004
+ "version": 2,
1005
+ "debugenabled": false,
1006
+ "outputs": 1,
1007
+ "entityType": "sensor",
1008
+ "config": [
1009
+ {
1010
+ "property": "name",
1011
+ "value": "PS Cap Increase Possible"
1012
+ },
1013
+ {
1014
+ "property": "device_class",
1015
+ "value": "power"
1016
+ },
1017
+ {
1018
+ "property": "icon",
1019
+ "value": ""
1020
+ },
1021
+ {
1022
+ "property": "unit_of_measurement",
1023
+ "value": "kW"
1024
+ },
1025
+ {
1026
+ "property": "state_class",
1027
+ "value": ""
1028
+ },
1029
+ {
1030
+ "property": "last_reset",
1031
+ "value": ""
1032
+ }
1033
+ ],
1034
+ "state": "payload.increasePossible",
1035
+ "stateType": "msg",
1036
+ "attributes": [],
1037
+ "resend": true,
1038
+ "outputLocation": "payload",
1039
+ "outputLocationType": "none",
1040
+ "inputOverride": "allow",
1041
+ "outputOnStateChange": false,
1042
+ "outputPayload": "",
1043
+ "outputPayloadType": "str",
1044
+ "x": 1100,
1045
+ "y": 1170,
1046
+ "wires": [[]]
1047
+ },
1048
+ {
1049
+ "id": "ac0b86c136f40790",
1050
+ "type": "function",
1051
+ "z": "d938c47f.3398f8",
1052
+ "name": "Reset Actions",
1053
+ "func": "\nconst actions = flow.get(\"actions\")\nconst ha = global.get(\"homeassistant\").homeAssistant\n\nconst BUFFER_TO_RESET = 1 // Must have 1kW extra to perform reset\n\nlet increasePossible = msg.payload.increasePossible\n\nif (increasePossible <= 0) {\n return null\n}\n\nfunction resetAction(action) {\n const info = {\n time: new Date().toISOString(),\n name: \"Reset action\",\n data: msg.paylaod,\n action\n }\n node.send([{ payload: action.payloadToResetAction}, {payload: info}])\n increasePossible -= action.savedConsumption\n action.actionTaken = false\n action.savedConsumption = 0\n flow.set(\"actions\", actions)\n}\n\nactions\n .filter(a => a.actionTaken\n && (a.savedConsumption + BUFFER_TO_RESET) <= increasePossible\n && (Date.now() - a.actionTime > a.minTimeOffSec * 1000)\n ).forEach(a => resetAction(a))\n",
1054
+ "outputs": 2,
1055
+ "noerr": 0,
1056
+ "initialize": "",
1057
+ "finalize": "",
1058
+ "libs": [],
1059
+ "x": 240,
1060
+ "y": 1270,
1061
+ "wires": [["28a20e58f1058b6d"], ["1d738e15969dd163"]]
1062
+ },
1063
+ {
1064
+ "id": "28a20e58f1058b6d",
1065
+ "type": "api-call-service",
1066
+ "z": "d938c47f.3398f8",
1067
+ "name": "Perform action",
1068
+ "server": "ec4a12a1.b2be9",
1069
+ "version": 5,
1070
+ "debugenabled": false,
1071
+ "domain": "",
1072
+ "service": "",
1073
+ "areaId": [],
1074
+ "deviceId": [],
1075
+ "entityId": [],
1076
+ "data": "",
1077
+ "dataType": "jsonata",
1078
+ "mergeContext": "",
1079
+ "mustacheAltTags": false,
1080
+ "outputProperties": [
1081
+ {
1082
+ "property": "payload",
1083
+ "propertyType": "msg",
1084
+ "value": "payload",
1085
+ "valueType": "msg"
1086
+ }
1087
+ ],
1088
+ "queue": "none",
1089
+ "x": 540,
1090
+ "y": 1240,
1091
+ "wires": [[]]
1092
+ },
1093
+ {
1094
+ "id": "1d738e15969dd163",
1095
+ "type": "file",
1096
+ "z": "d938c47f.3398f8",
1097
+ "name": "Save actions to file",
1098
+ "filename": "/share/capacity-actions.txt",
1099
+ "filenameType": "str",
1100
+ "appendNewline": true,
1101
+ "createDir": false,
1102
+ "overwriteFile": "false",
1103
+ "encoding": "none",
1104
+ "x": 540,
1105
+ "y": 1320,
1106
+ "wires": [[]]
1107
+ },
1108
+ {
1109
+ "id": "0656818b7253b0aa",
1110
+ "type": "catch",
1111
+ "z": "d938c47f.3398f8",
1112
+ "name": "Catch action errors",
1113
+ "scope": ["3cdb68064ac5a5bc", "ac0b86c136f40790"],
1114
+ "uncaught": false,
1115
+ "x": 250,
1116
+ "y": 1340,
1117
+ "wires": [["1d738e15969dd163"]]
1118
+ },
1119
+ {
1120
+ "id": "a614116d19af0b02",
1121
+ "type": "debug",
1122
+ "z": "d938c47f.3398f8",
1123
+ "name": "debug 2",
1124
+ "active": true,
1125
+ "tosidebar": true,
1126
+ "console": false,
1127
+ "tostatus": false,
1128
+ "complete": "false",
1129
+ "statusVal": "",
1130
+ "statusType": "auto",
1131
+ "x": 160,
1132
+ "y": 1110,
1133
+ "wires": []
1134
+ },
1135
+ {
1136
+ "id": "b70ec5d0.6f8f08",
1137
+ "type": "tibber-api-endpoint",
1138
+ "feedUrl": "wss://api.tibber.com/v1-beta/gql/subscriptions",
1139
+ "queryUrl": "https://api.tibber.com/v1-beta/gql",
1140
+ "name": "Tibber API"
1141
+ }
1142
+ ]