[
  {
    "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"
  }
]
