node-red-contrib-power-saver
Version:
A module for Node-RED that you can use to turn on and off a switch based on power prices
245 lines (244 loc) • 24.2 kB
JSON
[
{
"id": "2a073d402b1b6573",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Build query for consumption",
"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;",
"outputs": 1,
"noerr": 0,
"initialize": "// Code added here will be run once\n// whenever the node is started.\ncontext.set(\"previousHour\", undefined)",
"finalize": "",
"libs": [],
"x": 340,
"y": 1100,
"wires": [["cf42844ec5bdfd21"]]
},
{
"id": "cf42844ec5bdfd21",
"type": "tibber-query",
"z": "d938c47f.3398f8",
"name": "Get consumption",
"active": true,
"apiEndpointRef": "b70ec5d0.6f8f08",
"x": 150,
"y": 1180,
"wires": [["172bdb20196bc56a"]]
},
{
"id": "b5b84faebe49979e",
"type": "tibber-feed",
"z": "d938c47f.3398f8",
"name": "Get live data",
"active": true,
"apiEndpointRef": "b70ec5d0.6f8f08",
"homeId": "your-home-id-from-tibber",
"timestamp": "1",
"power": "1",
"lastMeterConsumption": false,
"accumulatedConsumption": true,
"accumulatedProduction": false,
"accumulatedConsumptionLastHour": "1",
"accumulatedProductionLastHour": false,
"accumulatedCost": false,
"accumulatedReward": false,
"currency": false,
"minPower": false,
"averagePower": false,
"maxPower": false,
"powerProduction": false,
"minPowerProduction": false,
"maxPowerProduction": false,
"lastMeterProduction": false,
"powerFactor": false,
"voltagePhase1": false,
"voltagePhase2": false,
"voltagePhase3": false,
"currentL1": false,
"currentL2": false,
"currentL3": false,
"signalStrength": false,
"x": 110,
"y": 1080,
"wires": [["90412687d7504168", "2a073d402b1b6573"]]
},
{
"id": "172bdb20196bc56a",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Find highest per day",
"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;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 1160,
"wires": [["deee9c5a2e504afd"]]
},
{
"id": "90412687d7504168",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Collect estimate for hour",
"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;",
"outputs": 1,
"noerr": 0,
"initialize": "// Code added here will be run once\n// whenever the node is started.\ncontext.set(\"buffer\", [])",
"finalize": "",
"libs": [],
"x": 330,
"y": 1060,
"wires": [["deee9c5a2e504afd"]]
},
{
"id": "deee9c5a2e504afd",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Calculate values",
"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;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 590,
"y": 1060,
"wires": [["ac0b86c136f40790", "3cdb68064ac5a5bc", "db2946b0d86cd4cd"]]
},
{
"id": "3cdb68064ac5a5bc",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Reduction Actions",
"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",
"outputs": 3,
"noerr": 0,
"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",
"finalize": "const actions = flow.get(\"actions\")\n\nactions\n .filter(a => a.actionTaken)\n .forEach(a => \n node.send({ payload: a.payloadToResetAction })\n )",
"libs": [],
"x": 460,
"y": 1270,
"wires": [["28a20e58f1058b6d"], ["c0f07cbad0e324dd", "2c50865d59881701"], ["1d738e15969dd163"]]
},
{
"id": "ac0b86c136f40790",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Reset Actions",
"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",
"outputs": 3,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 450,
"y": 1330,
"wires": [["28a20e58f1058b6d"], ["c0f07cbad0e324dd", "2c50865d59881701"], ["1d738e15969dd163"]]
},
{
"id": "28a20e58f1058b6d",
"type": "api-call-service",
"z": "d938c47f.3398f8",
"name": "Perform action",
"server": "ec4a12a1.b2be9",
"version": 5,
"debugenabled": false,
"domain": "",
"service": "",
"areaId": [],
"deviceId": [],
"entityId": [],
"data": "",
"dataType": "jsonata",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "payload",
"valueType": "msg"
}
],
"queue": "none",
"x": 770,
"y": 1360,
"wires": [[]]
},
{
"id": "1d738e15969dd163",
"type": "file",
"z": "d938c47f.3398f8",
"name": "Save actions to file",
"filename": "/share/capacity-actions.txt",
"filenameType": "str",
"appendNewline": true,
"createDir": false,
"overwriteFile": "false",
"encoding": "none",
"x": 780,
"y": 1420,
"wires": [[]]
},
{
"id": "0656818b7253b0aa",
"type": "catch",
"z": "d938c47f.3398f8",
"name": "Catch action errors",
"scope": ["3cdb68064ac5a5bc", "ac0b86c136f40790"],
"uncaught": false,
"x": 460,
"y": 1400,
"wires": [["1d738e15969dd163"]]
},
{
"id": "7f34cef20e2c0841",
"type": "ha-api",
"z": "d938c47f.3398f8",
"name": "Set entity",
"server": "ec4a12a1.b2be9",
"version": 1,
"debugenabled": false,
"protocol": "http",
"method": "post",
"path": "",
"data": "",
"dataType": "json",
"responseType": "json",
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "",
"valueType": "results"
}
],
"x": 990,
"y": 1060,
"wires": [[]]
},
{
"id": "db2946b0d86cd4cd",
"type": "function",
"z": "d938c47f.3398f8",
"name": "Update sensors",
"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",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 820,
"y": 1060,
"wires": [["7f34cef20e2c0841"]]
},
{
"id": "b70ec5d0.6f8f08",
"type": "tibber-api-endpoint",
"feedUrl": "wss://api.tibber.com/v1-beta/gql/subscriptions",
"queryUrl": "https://api.tibber.com/v1-beta/gql",
"name": "Tibber API"
}
]