node-red-contrib-power-saver 5.1.4 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/examples/add-general-tariff.json +120 -0
- package/examples/best-save-for-water-heater.json +122 -0
- package/examples/elvia-add-tariff.json +106 -0
- package/examples/elvia-get-tariff-types.json +58 -0
- package/examples/elvia-get-tariff.json +60 -0
- package/examples/example-grid-tariff-capacity-flow.json +244 -0
- package/examples/fixed-schedule-morning.json +138 -0
- package/examples/heat-capacitor-for-room-heating.json +186 -0
- package/examples/light-saver.json +162 -0
- package/examples/lowest-price-for-heating-cables.json +146 -0
- package/examples/price-filter.json +139 -0
- package/examples/schedule-merger.json +179 -0
- package/package.json +4 -2
- package/src/elvia/elvia-add-tariff.js +1 -1
- package/src/general-add-tariff-functions.js +3 -3
- package/src/general-add-tariff.html +2 -2
- package/src/handle-input.js +0 -1
- package/src/handle-output.js +0 -3
- package/src/light-saver.html +697 -646
- package/src/price-filter.html +72 -0
- package/src/price-filter.js +100 -0
- package/src/schedule-merger.js +2 -2
- package/src/strategy-best-save-functions.js +20 -5
- package/src/strategy-best-save.js +1 -1
- package/src/strategy-fixed-schedule.html +2 -2
- package/src/strategy-functions.js +1 -1
- package/src/strategy-heat-capacitor-functions.js +4 -4
- package/src/strategy-heat-capacitor.js +2 -2
- package/src/strategy-lowest-price.html +1 -2
- package/src/strategy-lowest-price.js +3 -4
- package/src/utils.js +4 -5
|
@@ -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// Allows records to deviate from maxAgeMs\nconst DELAY_TIME_MS_ALLOWED = 3 * 1000\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) + DELAY_TIME_MS_ALLOWED\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\nlet consumptionInPeriod = buffer[buffer.length - 1].accumulatedConsumption - buffer[0].accumulatedConsumption\nif (consumptionInPeriod < 0) {\nconsumptionInPeriod = 0\n}\nif (periodMs === 0) {\n //Should only occur during startup\n node.status({ fill: \"red\", shape: \"dot\", text: \"First item in buffer\" })\n return // Stopping rest of the flow for this message\n}\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Working\" })\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\nconst MIN_TIMELEFT = 30 //Min level for time left (30 seconds)\n\nconst ha = global.get(\"homeassistant\")[HA_NAME];\nif (!ha.isConnected) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"Ha not connected\" })\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 node.status({ fill: \"red\", shape: \"dot\", text: \"Time Left 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// Avoid calculations to increase too much when timeLeftSec is approaching zero\nconst minTimeLeftSec = Math.max(timeLeftSec, MIN_TIMELEFT);\n// Calculate reduction\nconst reductionRequired = alarmLevel < ALARM ? 0 :\n Math.max((currentMonthlyEstimate - currentStep) * highestCounting.length, 0)\n * 3600 / minTimeLeftSec;\nconst reductionRecommended = alarmLevel < 3 ? 0 :\n Math.max(hourEstimate + SAFE_ZONE - currentStep, 0)\n * 3600 / minTimeLeftSec;\n\n// Calculate increase possible\nconst increasePossible = alarmLevel >= 3 ? 0 :\n Math.max(currentStep - hourEstimate - SAFE_ZONE, 0)\n * 3600 / minTimeLeftSec;\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\nconst MIN_MINUTES_INTO_HOUR_TO_TAKE_ACTION = 5\n\nconst actions = flow.get(\"actions\")\nconst ha = global.get(\"homeassistant\").homeAssistant\n\nlet reductionRequired = msg.payload.reductionRequired\nlet reductionRecommended = msg.payload.reductionRecommended\n\nnode.status({})\n\nif(reductionRecommended <= 0 ) {\n return null\n}\n\nif (3600 - msg.payload.timeLeftSec < MIN_MINUTES_INTO_HOUR_TO_TAKE_ACTION * 60) {\n node.status({ fill: \"yellow\", shape: \"ring\", text: \"No action during first \" + MIN_MINUTES_INTO_HOUR_TO_TAKE_ACTION + \" minutes\"});\n return\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.payload,\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
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "fs01a2b3c4d5e6f7",
|
|
4
|
+
"type": "inject",
|
|
5
|
+
"z": "fs01tab01tab01t1",
|
|
6
|
+
"name": "Refresh prices (every hour)",
|
|
7
|
+
"props": [
|
|
8
|
+
{
|
|
9
|
+
"p": "payload"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"repeat": "3600",
|
|
13
|
+
"crontab": "",
|
|
14
|
+
"once": true,
|
|
15
|
+
"onceDelay": "1",
|
|
16
|
+
"topic": "",
|
|
17
|
+
"payload": "{viewer{homes{currentSubscription{priceInfo(resolution:QUARTER_HOURLY){today{totalstartsAt}tomorrow{totalstartsAt}}}}}}",
|
|
18
|
+
"payloadType": "str",
|
|
19
|
+
"x": 140,
|
|
20
|
+
"y": 80,
|
|
21
|
+
"wires": [["fs02a2b3c4d5e6f7"]]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "fs02a2b3c4d5e6f7",
|
|
25
|
+
"type": "tibber-query",
|
|
26
|
+
"z": "fs01tab01tab01t1",
|
|
27
|
+
"name": "Get Tibber prices",
|
|
28
|
+
"active": true,
|
|
29
|
+
"apiEndpointRef": "fs0btibberapi01",
|
|
30
|
+
"x": 360,
|
|
31
|
+
"y": 80,
|
|
32
|
+
"wires": [["fs03a2b3c4d5e6f7"]]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "fs03a2b3c4d5e6f7",
|
|
36
|
+
"type": "ps-receive-price",
|
|
37
|
+
"z": "fs01tab01tab01t1",
|
|
38
|
+
"name": "Price Receiver",
|
|
39
|
+
"x": 170,
|
|
40
|
+
"y": 160,
|
|
41
|
+
"wires": [["fs04a2b3c4d5e6f7"]]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "fs04a2b3c4d5e6f7",
|
|
45
|
+
"type": "ps-strategy-fixed-schedule",
|
|
46
|
+
"z": "fs01tab01tab01t1",
|
|
47
|
+
"name": "Morning On 06-08",
|
|
48
|
+
"periods": [
|
|
49
|
+
{
|
|
50
|
+
"start": "06",
|
|
51
|
+
"value": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"start": "08",
|
|
55
|
+
"value": false
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"days": {
|
|
59
|
+
"Mon": true,
|
|
60
|
+
"Tue": true,
|
|
61
|
+
"Wed": true,
|
|
62
|
+
"Thu": true,
|
|
63
|
+
"Fri": true,
|
|
64
|
+
"Sat": true,
|
|
65
|
+
"Sun": true
|
|
66
|
+
},
|
|
67
|
+
"validFrom": "",
|
|
68
|
+
"validTo": "",
|
|
69
|
+
"sendCurrentValueWhenRescheduling": true,
|
|
70
|
+
"outputValueForOn": true,
|
|
71
|
+
"outputValueForOff": false,
|
|
72
|
+
"outputValueForOntype": "bool",
|
|
73
|
+
"outputValueForOfftype": "bool",
|
|
74
|
+
"outputIfNoSchedule": "false",
|
|
75
|
+
"contextStorage": "default",
|
|
76
|
+
"x": 390,
|
|
77
|
+
"y": 160,
|
|
78
|
+
"wires": [["fs05a2b3c4d5e6f7"], ["fs06a2b3c4d5e6f7"], ["fs07a2b3c4d5e6f7"]]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "fs05a2b3c4d5e6f7",
|
|
82
|
+
"type": "debug",
|
|
83
|
+
"z": "fs01tab01tab01t1",
|
|
84
|
+
"name": "Turn ON",
|
|
85
|
+
"active": true,
|
|
86
|
+
"tosidebar": true,
|
|
87
|
+
"console": false,
|
|
88
|
+
"tostatus": false,
|
|
89
|
+
"complete": "payload",
|
|
90
|
+
"targetType": "msg",
|
|
91
|
+
"statusVal": "",
|
|
92
|
+
"statusType": "auto",
|
|
93
|
+
"x": 650,
|
|
94
|
+
"y": 100,
|
|
95
|
+
"wires": []
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "fs06a2b3c4d5e6f7",
|
|
99
|
+
"type": "debug",
|
|
100
|
+
"z": "fs01tab01tab01t1",
|
|
101
|
+
"name": "Turn OFF",
|
|
102
|
+
"active": true,
|
|
103
|
+
"tosidebar": true,
|
|
104
|
+
"console": false,
|
|
105
|
+
"tostatus": false,
|
|
106
|
+
"complete": "payload",
|
|
107
|
+
"targetType": "msg",
|
|
108
|
+
"statusVal": "",
|
|
109
|
+
"statusType": "auto",
|
|
110
|
+
"x": 650,
|
|
111
|
+
"y": 160,
|
|
112
|
+
"wires": []
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "fs07a2b3c4d5e6f7",
|
|
116
|
+
"type": "debug",
|
|
117
|
+
"z": "fs01tab01tab01t1",
|
|
118
|
+
"name": "Fixed Schedule",
|
|
119
|
+
"active": true,
|
|
120
|
+
"tosidebar": true,
|
|
121
|
+
"console": false,
|
|
122
|
+
"tostatus": false,
|
|
123
|
+
"complete": "payload",
|
|
124
|
+
"targetType": "msg",
|
|
125
|
+
"statusVal": "",
|
|
126
|
+
"statusType": "auto",
|
|
127
|
+
"x": 650,
|
|
128
|
+
"y": 220,
|
|
129
|
+
"wires": []
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "fs0btibberapi01",
|
|
133
|
+
"type": "tibber-api-endpoint",
|
|
134
|
+
"feedUrl": "wss://api.tibber.com/v1-beta/gql/subscriptions",
|
|
135
|
+
"queryUrl": "https://api.tibber.com/v1-beta/gql",
|
|
136
|
+
"name": "Tibber API"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "135c4e7649611314",
|
|
4
|
+
"type": "tab",
|
|
5
|
+
"label": "PowerSaver",
|
|
6
|
+
"disabled": false,
|
|
7
|
+
"info": "",
|
|
8
|
+
"env": []
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "cf5908a52e0aee5e",
|
|
12
|
+
"type": "ps-receive-price",
|
|
13
|
+
"z": "135c4e7649611314",
|
|
14
|
+
"name": "Price Receiver",
|
|
15
|
+
"x": 400,
|
|
16
|
+
"y": 320,
|
|
17
|
+
"wires": [["b7b85590b7d28ba6"]]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "b08bc12bf8734c5a",
|
|
21
|
+
"type": "tibber-query",
|
|
22
|
+
"z": "135c4e7649611314",
|
|
23
|
+
"name": "",
|
|
24
|
+
"active": true,
|
|
25
|
+
"apiEndpointRef": "9ea07b03b88cb526",
|
|
26
|
+
"x": 230,
|
|
27
|
+
"y": 320,
|
|
28
|
+
"wires": [["cf5908a52e0aee5e"]]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "d0d4dd31efe67e85",
|
|
32
|
+
"type": "inject",
|
|
33
|
+
"z": "135c4e7649611314",
|
|
34
|
+
"name": "",
|
|
35
|
+
"props": [
|
|
36
|
+
{
|
|
37
|
+
"p": "payload"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"p": "topic",
|
|
41
|
+
"vt": "str"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"repeat": "60",
|
|
45
|
+
"crontab": "",
|
|
46
|
+
"once": true,
|
|
47
|
+
"onceDelay": "1",
|
|
48
|
+
"topic": "",
|
|
49
|
+
"payload": "{viewer{homes{currentSubscription{priceInfo{today{totalstartsAt}tomorrow{totalstartsAt}}}}}}",
|
|
50
|
+
"payloadType": "str",
|
|
51
|
+
"x": 90,
|
|
52
|
+
"y": 320,
|
|
53
|
+
"wires": [["b08bc12bf8734c5a"]]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "4831f393a0066565",
|
|
57
|
+
"type": "api-call-service",
|
|
58
|
+
"z": "135c4e7649611314",
|
|
59
|
+
"name": "Set temperature",
|
|
60
|
+
"server": "e2dd69fb.8f70a8",
|
|
61
|
+
"version": 3,
|
|
62
|
+
"debugenabled": false,
|
|
63
|
+
"service_domain": "climate",
|
|
64
|
+
"service": "set_temperature",
|
|
65
|
+
"entityId": "climate.my_climate",
|
|
66
|
+
"data": "{\"entity_id\":\"climate.my_climate\",\"temperature\":\"{{adj_setpoint}}\"}",
|
|
67
|
+
"dataType": "json",
|
|
68
|
+
"mergecontext": "",
|
|
69
|
+
"mustacheAltTags": false,
|
|
70
|
+
"outputProperties": [],
|
|
71
|
+
"queue": "none",
|
|
72
|
+
"x": 980,
|
|
73
|
+
"y": 320,
|
|
74
|
+
"wires": [[]]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "027f4267d969e1b8",
|
|
78
|
+
"type": "server-state-changed",
|
|
79
|
+
"z": "135c4e7649611314",
|
|
80
|
+
"name": "Setpoint",
|
|
81
|
+
"server": "e2dd69fb.8f70a8",
|
|
82
|
+
"version": 3,
|
|
83
|
+
"exposeToHomeAssistant": false,
|
|
84
|
+
"haConfig": [
|
|
85
|
+
{
|
|
86
|
+
"property": "name",
|
|
87
|
+
"value": ""
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"property": "icon",
|
|
91
|
+
"value": ""
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"entityidfilter": "input_number.setpoint",
|
|
95
|
+
"entityidfiltertype": "exact",
|
|
96
|
+
"outputinitially": true,
|
|
97
|
+
"state_type": "num",
|
|
98
|
+
"haltifstate": "",
|
|
99
|
+
"halt_if_type": "str",
|
|
100
|
+
"halt_if_compare": "is",
|
|
101
|
+
"outputs": 1,
|
|
102
|
+
"output_only_on_state_change": false,
|
|
103
|
+
"for": 0,
|
|
104
|
+
"forType": "num",
|
|
105
|
+
"forUnits": "minutes",
|
|
106
|
+
"ignorePrevStateNull": false,
|
|
107
|
+
"ignorePrevStateUnknown": false,
|
|
108
|
+
"ignorePrevStateUnavailable": false,
|
|
109
|
+
"ignoreCurrentStateUnknown": true,
|
|
110
|
+
"ignoreCurrentStateUnavailable": true,
|
|
111
|
+
"outputProperties": [
|
|
112
|
+
{
|
|
113
|
+
"property": "payload.config.setpoint",
|
|
114
|
+
"propertyType": "msg",
|
|
115
|
+
"value": "",
|
|
116
|
+
"valueType": "entityState"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"property": "data",
|
|
120
|
+
"propertyType": "msg",
|
|
121
|
+
"value": "",
|
|
122
|
+
"valueType": "eventData"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"property": "topic",
|
|
126
|
+
"propertyType": "msg",
|
|
127
|
+
"value": "",
|
|
128
|
+
"valueType": "triggerId"
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
"x": 420,
|
|
132
|
+
"y": 360,
|
|
133
|
+
"wires": [["b7b85590b7d28ba6"]]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "b7b85590b7d28ba6",
|
|
137
|
+
"type": "ps-strategy-heat-capacitor",
|
|
138
|
+
"z": "135c4e7649611314",
|
|
139
|
+
"name": "Heat capacitor",
|
|
140
|
+
"timeHeat1C": "70",
|
|
141
|
+
"timeCool1C": 50,
|
|
142
|
+
"maxTempAdjustment": "1",
|
|
143
|
+
"boostTempHeat": "2",
|
|
144
|
+
"boostTempCool": "2",
|
|
145
|
+
"minSavings": 0.08,
|
|
146
|
+
"setpoint": 23,
|
|
147
|
+
"x": 600,
|
|
148
|
+
"y": 320,
|
|
149
|
+
"wires": [["2b7cbdef3203a482"], [], []]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "2b7cbdef3203a482",
|
|
153
|
+
"type": "function",
|
|
154
|
+
"z": "135c4e7649611314",
|
|
155
|
+
"name": "Adjust setpoint",
|
|
156
|
+
"func": "//In case the climate entity can only handle integers\n//Calculate rounded setpoint for the climate entity and return the msg\nmsg.adj_setpoint=Math.round(msg.payload);\nreturn msg\n",
|
|
157
|
+
"outputs": 1,
|
|
158
|
+
"noerr": 0,
|
|
159
|
+
"initialize": "",
|
|
160
|
+
"finalize": "",
|
|
161
|
+
"libs": [],
|
|
162
|
+
"x": 800,
|
|
163
|
+
"y": 320,
|
|
164
|
+
"wires": [["4831f393a0066565"]]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"id": "9ea07b03b88cb526",
|
|
168
|
+
"type": "tibber-api-endpoint",
|
|
169
|
+
"feedUrl": "wss://api.tibber.com/v1-beta/gql/subscriptions",
|
|
170
|
+
"queryUrl": "https://api.tibber.com/v1-beta/gql",
|
|
171
|
+
"name": "Tibber"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "e2dd69fb.8f70a8",
|
|
175
|
+
"type": "server",
|
|
176
|
+
"name": "Home Assistant",
|
|
177
|
+
"version": 2,
|
|
178
|
+
"addon": false,
|
|
179
|
+
"rejectUnauthorizedCerts": true,
|
|
180
|
+
"ha_boolean": "y|yes|true|on|home|open",
|
|
181
|
+
"connectionDelay": false,
|
|
182
|
+
"cacheJson": true,
|
|
183
|
+
"heartbeat": false,
|
|
184
|
+
"heartbeatInterval": 30
|
|
185
|
+
}
|
|
186
|
+
]
|