node-red-contrib-power-saver 4.1.0 → 4.1.2

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.
@@ -0,0 +1,244 @@
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 receive 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 = \"142c4839-64cf-4df4-ba6d-942527a757c4\"\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": 340,
14
+ "y": 1100,
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": 150,
25
+ "y": 1180,
26
+ "wires": [["172bdb20196bc56a"]]
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": "your-home-id-from-tibber",
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": 110,
62
+ "y": 1080,
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": 380,
77
+ "y": 1160,
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": 330,
92
+ "y": 1060,
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 = [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 highestTodayConsumption: highestToday.consumption,\n highestTodayFrom: highestToday.from,\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": 590,
107
+ "y": 1060,
108
+ "wires": [["ac0b86c136f40790", "3cdb68064ac5a5bc", "db2946b0d86cd4cd"]]
109
+ },
110
+ {
111
+ "id": "3cdb68064ac5a5bc",
112
+ "type": "function",
113
+ "z": "d938c47f.3398f8",
114
+ "name": "Reduction Actions",
115
+ "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\n // output1 is for actions\n const output1 = action.payloadToTakeAction ? { payload: action.payloadToTakeAction } : null\n // output 2 is for overriding PS strategies\n const output2 = action.nameOfStrategyToOverride ? { payload: { config: { override: \"off\" }, name: action.nameOfStrategyToOverride} } : null\n // output 3 is for logging\n const output3 = { payload: info }\n\n node.send([output1, output2, output3])\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 / 1000\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",
116
+ "outputs": 3,
117
+ "noerr": 0,
118
+ "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 nameOfStrategyToOverride: \"Best Save\",\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// 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",
119
+ "finalize": "const actions = flow.get(\"actions\")\n\nactions\n .filter(a => a.actionTaken)\n .forEach(a => \n node.send({ payload: a.payloadToResetAction })\n )",
120
+ "libs": [],
121
+ "x": 460,
122
+ "y": 1270,
123
+ "wires": [["28a20e58f1058b6d"], ["c0f07cbad0e324dd", "2c50865d59881701"], ["1d738e15969dd163"]]
124
+ },
125
+ {
126
+ "id": "ac0b86c136f40790",
127
+ "type": "function",
128
+ "z": "d938c47f.3398f8",
129
+ "name": "Reset Actions",
130
+ "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 const output1 = action.payloadToResetAction ? { payload: action.payloadToResetAction } : null\n const output2 = action.nameOfStrategyToOverride ? { payload: { config: { override: \"auto\" }, name: action.nameOfStrategyToOverride } } : null\n const output3 = { payload: info }\n\n node.send([output1, output2, output3])\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",
131
+ "outputs": 3,
132
+ "noerr": 0,
133
+ "initialize": "",
134
+ "finalize": "",
135
+ "libs": [],
136
+ "x": 450,
137
+ "y": 1330,
138
+ "wires": [["28a20e58f1058b6d"], ["c0f07cbad0e324dd", "2c50865d59881701"], ["1d738e15969dd163"]]
139
+ },
140
+ {
141
+ "id": "28a20e58f1058b6d",
142
+ "type": "api-call-service",
143
+ "z": "d938c47f.3398f8",
144
+ "name": "Perform action",
145
+ "server": "ec4a12a1.b2be9",
146
+ "version": 5,
147
+ "debugenabled": false,
148
+ "domain": "",
149
+ "service": "",
150
+ "areaId": [],
151
+ "deviceId": [],
152
+ "entityId": [],
153
+ "data": "",
154
+ "dataType": "jsonata",
155
+ "mergeContext": "",
156
+ "mustacheAltTags": false,
157
+ "outputProperties": [
158
+ {
159
+ "property": "payload",
160
+ "propertyType": "msg",
161
+ "value": "payload",
162
+ "valueType": "msg"
163
+ }
164
+ ],
165
+ "queue": "none",
166
+ "x": 770,
167
+ "y": 1360,
168
+ "wires": [[]]
169
+ },
170
+ {
171
+ "id": "1d738e15969dd163",
172
+ "type": "file",
173
+ "z": "d938c47f.3398f8",
174
+ "name": "Save actions to file",
175
+ "filename": "/share/capacity-actions.txt",
176
+ "filenameType": "str",
177
+ "appendNewline": true,
178
+ "createDir": false,
179
+ "overwriteFile": "false",
180
+ "encoding": "none",
181
+ "x": 780,
182
+ "y": 1420,
183
+ "wires": [[]]
184
+ },
185
+ {
186
+ "id": "0656818b7253b0aa",
187
+ "type": "catch",
188
+ "z": "d938c47f.3398f8",
189
+ "name": "Catch action errors",
190
+ "scope": ["3cdb68064ac5a5bc", "ac0b86c136f40790"],
191
+ "uncaught": false,
192
+ "x": 460,
193
+ "y": 1400,
194
+ "wires": [["1d738e15969dd163"]]
195
+ },
196
+ {
197
+ "id": "7f34cef20e2c0841",
198
+ "type": "ha-api",
199
+ "z": "d938c47f.3398f8",
200
+ "name": "Set entity",
201
+ "server": "ec4a12a1.b2be9",
202
+ "version": 1,
203
+ "debugenabled": false,
204
+ "protocol": "http",
205
+ "method": "post",
206
+ "path": "",
207
+ "data": "",
208
+ "dataType": "json",
209
+ "responseType": "json",
210
+ "outputProperties": [
211
+ {
212
+ "property": "payload",
213
+ "propertyType": "msg",
214
+ "value": "",
215
+ "valueType": "results"
216
+ }
217
+ ],
218
+ "x": 990,
219
+ "y": 1060,
220
+ "wires": [[]]
221
+ },
222
+ {
223
+ "id": "db2946b0d86cd4cd",
224
+ "type": "function",
225
+ "z": "d938c47f.3398f8",
226
+ "name": "Update sensors",
227
+ "func": "const sensors = [\n { id: \"sensor.ps_cap_status\", value: \"status\", uom: null },\n { id: \"binary_sensor.ps_cap_ok\", value: \"statusOk\", uom: null },\n { id: \"binary_sensor.ps_cap_warning\", value: \"statusWarning\", uom: null },\n { id: \"binary_sensor.ps_cap_alarm\", value: \"statusAlarm\", uom: null },\n { id: \"sensor.ps_cap_alarm_level\", value: \"alarmLevel\", uom: null },\n { id: \"sensor.ps_cap_current_step\", value: \"currentStep\", uom: \"kW\" },\n { id: \"sensor.ps_cap_hour_estimate\", value: \"hourEstimate\", uom: \"kW\" },\n { id: \"sensor.ps_cap_current_hour_ranking\", value: \"currentHourRanking\", uom: null },\n { id: \"sensor.ps_cap_monthly_estimate\", value: \"currentMonthlyEstimate\", uom: \"kW\" },\n { id: \"sensor.ps_cap_highest_today\", value: \"highestTodayConsumption\", uom: \"kW\" },\n { id: \"sensor.ps_cap_highest_today_time\", value: \"highestTodayFrom\", uom: null },\n { id: \"sensor.ps_cap_reduction_required\", value: \"reductionRequired\", uom: \"kW\" },\n { id: \"sensor.ps_cap_reduction_recommended\", value: \"reductionRecommended\", uom: \"kW\" },\n { id: \"sensor.ps_cap_increase_possible\", value: \"increasePossible\", uom: \"kW\" },\n { id: \"sensor.ps_cap_estimate_rest_of_hour\", value: \"consumptionLeft\", uom: \"kW\" },\n { id: \"sensor.ps_cap_consumption_accumulated_hour\", value: \"accumulatedConsumptionLastHour\", uom: \"kW\" },\n { id: \"sensor.ps_cap_time_left\", value: \"timeLeftSec\", uom: \"s\" },\n { id: \"sensor.ps_cap_consumption_now\", value: \"averageConsumptionNow\", uom: \"kW\" },\n]\n\nsensors.forEach((sensor) => {\n const payload = {\n protocol: \"http\",\n method: \"post\",\n path: \"/api/states/\" + sensor.id,\n data: {\n state: msg.payload[sensor.value],\n attributes: { unit_of_measurement: sensor.uom }\n }\n }\n node.send({payload})\n})\n",
228
+ "outputs": 1,
229
+ "noerr": 0,
230
+ "initialize": "",
231
+ "finalize": "",
232
+ "libs": [],
233
+ "x": 820,
234
+ "y": 1060,
235
+ "wires": [["7f34cef20e2c0841"]]
236
+ },
237
+ {
238
+ "id": "b70ec5d0.6f8f08",
239
+ "type": "tibber-api-endpoint",
240
+ "feedUrl": "wss://api.tibber.com/v1-beta/gql/subscriptions",
241
+ "queryUrl": "https://api.tibber.com/v1-beta/gql",
242
+ "name": "Tibber API"
243
+ }
244
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-saver",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "A module for Node-RED that you can use to turn on and off a switch based on power prices",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -54,15 +54,15 @@
54
54
  "eslint": "^8.25.0",
55
55
  "expect": "^27.5.1",
56
56
  "mocha": "^9.2.0",
57
- "node-red": "^2.2.2",
57
+ "node-red": "^3.0.2",
58
58
  "node-red-node-test-helper": "^0.2.7",
59
59
  "sass-loader": "^12.6.0",
60
60
  "vuepress": "^2.0.0-beta.36"
61
61
  },
62
62
  "dependencies": {
63
- "floating-vue": "^2.0.0-beta.6",
63
+ "floating-vue": "^2.0.0-beta.20",
64
64
  "lodash.clonedeep": "^4.5.0",
65
- "luxon": "^2.3.1",
65
+ "luxon": "^3.1.0",
66
66
  "nano-time": "^1.0.0",
67
67
  "node-fetch": "^2.6.7"
68
68
  }
@@ -3,11 +3,15 @@ const fetch = require("node-fetch");
3
3
  function ping(node, subscriptionKey, setResultStatus = true) {
4
4
  const url = "https://elvia.azure-api.net/grid-tariff/Ping";
5
5
  const headers = { "X-API-Key": subscriptionKey };
6
- fetch(url, { headers }).then((res) => {
7
- if (setResultStatus) {
8
- setNodeStatus(node, res.status);
9
- }
10
- });
6
+ fetch(url, { headers })
7
+ .then((res) => {
8
+ if (setResultStatus) {
9
+ setNodeStatus(node, res.status);
10
+ }
11
+ })
12
+ .catch((e) => {
13
+ console.log("Elvia API error: " + e);
14
+ });
11
15
  }
12
16
 
13
17
  function getTariff(node, subscriptionKey, tariffKey, range = "today", setResultStatus = true) {
@@ -34,21 +38,25 @@ function getTariffTypes(node, subscriptionKey, setResultStatus = true) {
34
38
 
35
39
  function get(node, subscriptionKey, url, setResultStatus) {
36
40
  const headers = { "X-API-Key": subscriptionKey };
37
- return fetch(url, { headers }).then((res) => {
38
- if (setResultStatus && node) {
39
- setNodeStatus(node, res.status);
40
- }
41
- if (res.status === 500) {
42
- console.error("Elvia internal server error (status 500)");
43
- return;
44
- }
45
- return res.json().then((json) => {
46
- if (json.statusCode === 401) {
47
- console.error("Elvia API error: " + json.message);
41
+ return fetch(url, { headers })
42
+ .then((res) => {
43
+ if (setResultStatus && node) {
44
+ setNodeStatus(node, res.status);
45
+ }
46
+ if (res.status === 500) {
47
+ console.error("Elvia internal server error (status 500)");
48
+ return;
48
49
  }
49
- return json;
50
+ return res.json().then((json) => {
51
+ if (json.statusCode === 401) {
52
+ console.error("Elvia API error: " + json.message);
53
+ }
54
+ return json;
55
+ });
56
+ })
57
+ .catch((e) => {
58
+ console.log("Elvia API error: " + e);
50
59
  });
51
- });
52
60
  }
53
61
 
54
62
  function setNodeStatus(node, status) {
@@ -1,6 +1,4 @@
1
- const fetch = require("node-fetch");
2
-
3
- const { getTariff, ping } = require("./elvia-api.js");
1
+ const { getTariff, ping } = require("./elvia-api");
4
2
 
5
3
  module.exports = function (RED) {
6
4
  function PsElviaTariffNode(config) {
@@ -59,8 +59,8 @@ function handleOutput(node, config, plan, outputCommands, planFromTime) {
59
59
  }
60
60
 
61
61
  function sendSwitch(node, onOff) {
62
- const output1 = onOff ? { payload: true } : null;
63
- const output2 = onOff ? null : { payload: false };
62
+ const output1 = onOff ? { payload: node.outputValueForOn } : null;
63
+ const output2 = onOff ? null : { payload: node.outputValueForOff };
64
64
  node.send([output1, output2, null]);
65
65
  node.context().set("currentOutput", onOff);
66
66
  }
@@ -171,12 +171,10 @@ describe("ps-strategy-best-save node", function () {
171
171
  }
172
172
  });
173
173
  n3.on("input", function (msg) {
174
- console.log("on");
175
174
  countOn++;
176
175
  expect(msg).toHaveProperty("payload", true);
177
176
  });
178
177
  n4.on("input", function (msg) {
179
- console.log("off");
180
178
  countOff++;
181
179
  expect(msg).toHaveProperty("payload", false);
182
180
  if (countOff === 2) {
@@ -186,6 +184,66 @@ describe("ps-strategy-best-save node", function () {
186
184
  n1.receive({ payload: makePayload(prices, plan.time) });
187
185
  });
188
186
  });
187
+ it("should send number as output", function (done) {
188
+ const flow = makeFlow(3, 2);
189
+ flow[0].outputValueForOn = "1";
190
+ flow[0].outputValueForOff = "0";
191
+ flow[0].outputValueForOntype = "num";
192
+ flow[0].outputValueForOfftype = "num";
193
+ helper.load(bestSave, flow, function () {
194
+ const n1 = helper.getNode("n1");
195
+ const n2 = helper.getNode("n2");
196
+ const n3 = helper.getNode("n3");
197
+ const n4 = helper.getNode("n4");
198
+ n2.on("input", function (msg) {
199
+ expect(msg.payload.config.outputValueForOn).toEqual(1);
200
+ expect(msg.payload.config.outputValueForOff).toEqual(0);
201
+ expect(msg.payload.config.outputValueForOntype).toEqual("num");
202
+ expect(msg.payload.config.outputValueForOfftype).toEqual("num");
203
+ n1.warn.should.not.be.called;
204
+ setTimeout(() => {
205
+ done();
206
+ }, 100);
207
+ });
208
+ n3.on("input", function (msg) {
209
+ expect(msg).toHaveProperty("payload", 1);
210
+ });
211
+ n4.on("input", function (msg) {
212
+ expect(msg).toHaveProperty("payload", 0);
213
+ });
214
+ n1.receive({ payload: makePayload(prices, plan.time) });
215
+ });
216
+ });
217
+ it("should send text as output", function (done) {
218
+ const flow = makeFlow(3, 2);
219
+ flow[0].outputValueForOn = "on";
220
+ flow[0].outputValueForOff = "off";
221
+ flow[0].outputValueForOntype = "str";
222
+ flow[0].outputValueForOfftype = "str";
223
+ helper.load(bestSave, flow, function () {
224
+ const n1 = helper.getNode("n1");
225
+ const n2 = helper.getNode("n2");
226
+ const n3 = helper.getNode("n3");
227
+ const n4 = helper.getNode("n4");
228
+ n2.on("input", function (msg) {
229
+ expect(msg.payload.config.outputValueForOn).toEqual("on");
230
+ expect(msg.payload.config.outputValueForOff).toEqual("off");
231
+ expect(msg.payload.config.outputValueForOntype).toEqual("str");
232
+ expect(msg.payload.config.outputValueForOfftype).toEqual("str");
233
+ n1.warn.should.not.be.called;
234
+ setTimeout(() => {
235
+ done();
236
+ }, 100);
237
+ });
238
+ n3.on("input", function (msg) {
239
+ expect(msg).toHaveProperty("payload", "on");
240
+ });
241
+ n4.on("input", function (msg) {
242
+ expect(msg).toHaveProperty("payload", "off");
243
+ });
244
+ n1.receive({ payload: makePayload(prices, plan.time) });
245
+ });
246
+ });
189
247
  });
190
248
 
191
249
  function makePayload(prices, time) {