Capacity part of grid tariff
Introduction
I Norway, there has been introduced a monthly fee for grid capacity. The purpose is to get people to avoid peaks of high capacity usage.
The fee is calculated based on the average consumption per hour (kWh). For each day, the worst hour is used, that is the hour with highest consumption that day. Then the 3 days where this number is highest (the 3 highest) is used. The average of these 3 days is calculated, and the result decides what capacity step you will pay for.
The steps are for example like this:
- 0-2 kW
- 2-5 kW
- 5-10 kW
- 10-15 kW
- 15-20 kW
This may be different for different grid providers.
If, for example, the average of the 3 worst days is 6 kW, you pay for step 5-10 kW.
Here is an example how this can be controlled. The example contains several features, and you may not want to use them all, so it is a good idea to read through it all before you decide how to use it.
The first part of nodes (upper part) is used to read consumption from Tibber, and to perform all calculations. The upper right two nodes are used to update a set of sensors in HA. You can use those sensor for many purposes related to the grid capacity. The bottom part is used to take actions in order to reduce power consumption, or to reset actions when the consumption is lowered.
You may use the whole example, or only part of it, so you should read through the whole documentation before you start, so you can decide what to use and how to use it.
The complex parts are solved by scripts in the function nodes, and you will have to change parts of them to use it all.
No guarantee
There is no guarantee that this works, so use at your own risk.
Requirements
You need the following to be able to use this example:
- Home Assistant with Node-RED
- Tibber Pulse to measure consumption continuously.
- Tibber subscription to get consumption data per hour.
- Token to access Tibber API.
- Node-RED Companion Integration
- Tibber nodes in Node-RED (node-red-contrib-tibber-api).
If you have the same information from other sources, you may be able to adapt the example.
Features
- 15-20 sensors in HA showing information related to the capacity and the calculation, including:
- Status Ok, Warning or Alarm related to current consumption compared to the next step.
- Alarm level 0-9 based on how serious the current consumption is.
- Current step based on usage the whole month.
- The highest consumption any hour today.
- Reduction required to avoid breaking the limit.
- Reduction recommended to reduce risk of breaking he limit.
- Estimates for the current hour.
- Current consumption based on average the last minute (can be configured).
- Automatically take actions to reduce consumption if it is recommended or required.
- Automatically reset actions if the consumption is sufficiently reduced.
- Log actions taken to a file
- Actions may also be done by directly and automatically overriding strategy nodes.
Algorithm
The consumption is measured by Tibber Pulse continuously (every 2 seconds), and is used to calculate average consumption the last minute. (Can be configured to another number of minutes.) It is assumed that you will have this consumption the rest of the current hour. Based on the real consumption until now this hour, and the assumed consumption the rest of the hour, the total for the current hour is estimated. This is the hourEstimate
.
The highest hour previously this day is found as highestToday
.
The highest hour for each day this month is found, and the 3 highest is used in the calculation as the highestCounting
.
Then the hourEstimate
is compared to those other numbers to calculate an alarm level.
If the current hour is todays worst, it is ranked together with the other days this month. If today is ranked as 4 or better, where 1 is the worst this month, then status is Ok. It will not affect the fee.
The currentStep
is decided based on the 3 worst days, including today, but not considering the current hour.
If today is ranked as 1, 2 or 3, it may affect the fee. Then the average of the three worst days, including this hour, is calculated. This is currentMonthlyEstimate
.
Other sensor values are calculated based on values available. See description for each of them for details.
Calculated sensor values
The Calculate values
node creates a payload with values that are used to create sensors in Home Assistant. The Update sensors
node maps these values to entity ids, and the Set entity
node uses the HA API to update sensors.
Here is a description of each of those sensor values.
Status
The status of the current hour as text: Ok
, Warning
or Alarm
.
Alarm
means that if you do not reduce the consumption, you will break the limit of the next step.
Warning
means that there is an increased risk for breaking the limit.
Ok
means you are pretty safe.
Ok
A boolean value, true
if the status is Ok
, false
if not.
Warning
A boolean value, true
if the status is Warning
, false
if not.
Alarm
A boolean value, true
if the status is Alarm
, false
if not.
You can configure the constant ALARM
to the lowest level that will cause status Alarm
. Default is 8
, meaning you have a small buffer (the BUFFER
).
Alarm Level
A number from 0 to 9, meaning as follows:
Level | Description |
---|---|
0 | You're good, either because consumption is low or because there was a worse hour earlier today. |
9 | The current consumption will lead to breaking the limit for the next step, if continued. |
8 | Same as 9 but with a buffer you can configure to reduce risk. The buffer is a constant named BUFFER , and the default value is 0.5 kWh |
7 | The hourEstimate (estimate for the hour) is higher than the limit for the next step, but since previous consumption has been low, the average of the 3 worst is below the limit, so you will not break the limit yet (!). |
6 | Same as 7 but with a the same buffer as for alarm level 8 |
5 | The hourEstimate is the worst this month, and it is only a safe zone away from breaking the limit. The safe zone can be configured using the constant called SAFE_ZONE , default 2 kWh. |
4 | The hourEstimate is the second to worst this month, and it is only a safe zone away from breaking the limit. |
3 | The hourEstimate is the third to worst this month, and it is only a safe zone away from breaking the limit. |
2 | The hourEstimate is the worst this month. Consumption may still be low. |
1 | The hourEstimate is the second to worst this month. Consumption may still be low. |
Current Step
Shows the step you are currently on (under), that is the limit you do not want to break. The steps are configured using the constant STEPS
in the script. The default is
const STEPS = [2, 5, 10, 15, 20]
Configure steps
You must edit the STEPS
constant in the script. Set it to the relevant steps for your grid supplier. You should omit steps that you do not plan to stay under, so you avoid alarms and actions for that step in the beginning of the month.
Hour Estimate
The estimated consumption for the current hour (hourEstimate
). This is calculated as real consumption earlier this hour plus estimated consumption for the rest of the hour.
Current Hour Ranking
A number (0-4) ranking the current hour with the 3 highest hours this month.
The current hour ranking has the following meaning:
Value | Description |
---|---|
0 | Not counting, since there has been another hour earlier today that is higher. |
1 | This is estimated to be the worst hour in the month. |
2 | This is estimated to be the second worst hour in the month. |
3 | This is estimated to be the third worst hour in the month. |
4 | This hour is estimated to be the worst today, but not one of the top 3 this month. |
Max ranking value
The highest value here, 4, is based on the value of MAX_COUNTING
in the Find highest per day
node. If you change MAX_COUNTING
, the values here will also change.
Monthly Estimate
The average of the 3 worst hours so far this month, in kWh.
Highest Today
The consumption in kWh of the worst hour today, not including the current hour.
Highest Today Hour
The start time of the worst hour today (Highest Today).
Reduction Required
The number of kW you must reduce the rest of the hour in order to not get alarm (break the limit), based on hourEstimate
.
This follows the Alarm
state, and thus also the ALARM
constant, so if alarm level 8
gives status Alarm
, reduction required is also set when the alarm level is 8
. However, if you reconfigure ALARM
to be only on level 9
, then reduction required is only set when the alarm level is 9
.
Please note that this takes the time in consideration, so if the hourEstimate
is 1 kWh over the limit, and the time left of the hour is 15 minutes, the reduction required will be 4 kW. You must reduce the consumption with 4kW in order to save 1 kWh during 15 minutes.
Reduction Recommended
The number of kW you must reduce the rest of the hour in order to stay under alarm level 3, based on hourEstimate
. Calculated the same way as reduction required.
Increase Possible
The number of kW you can increase consumption and still stay within the configured safe zone. If alarm level is > 2, this value will be 0. You will never have increase possible at the same time as reduction recommended (nor reduction required).
Estimate Rest Of Hour
The estimated consumption in kWh for the rest of the hour.
Consumption Accumulated Hour
The actual consumption in kWh until now this hour.
Time Left
The time left of the current hour in seconds.
Consumption Now
The current consumption, measured as average the last minute. Can be set to another number of minutes using the ESTIMATION_TIME_MINUTES
constant in the Collect estimate for hour
node.
Node description
Here is a description of each node in the example.
Get live data
This is a tibber-feed
node. It sets up a subscription of live Tibber data from Tibber Pulse, and uses this data to run the automation. Tick the following check boxes:
- Timestamp
- Power
- Accumulated consumption
- Accumulated consumption last hour
Uncheck all the others.
If you do not use Tibber, but can get this information from another source, convert it to the following format and use it the same way as data from this node is used:
{
"timestamp": "2022-06-12T14:42:00.000+02:00",
"power": 9503,
"accumulatedConsumption": 33.459665,
"accumulatedConsumptionLastHour": 7.046665
}
Build query for consumption
This is a function node that is used to build a Tibber query. It runs for all the live data, takes the time and calculates how many hours there has been since the beginning of the month. It uses this number to build a Tibber query to get consumption per hour since the beginning of the month. It sends output only when first started and when the hour changes, so it initiates a Tibber query once per hour.
Tibber Home Id
This node needs the tibber home id, so you must find it in the Tibber Developer Pages and set the vale of TIBBER_HOME_ID
in the beginning of the code.
Code
Get consumption
This is a tibber-query
node used to get consumption per hour for passed hours. It takes a Tibber query as input, and sends the result as output. The query is built by the previous node.
If you do not use Tibber, but can get this information from another source, convert it to the following format and use it the same way as data from this node is used:
{
"viewer": {
"home": {
"consumption": {
"nodes": [
{
"from": "2022-06-01T00:00:00.000+02:00",
"consumption": 4.307
},
{
"from": "2022-06-01T01:00:00.000+02:00",
"consumption": 3.648
},
{
"from": "2022-06-01T02:00:00.000+02:00",
"consumption": 2.406
},
// ...
{
"from": "2022-06-12T12:00:00.000+02:00",
"consumption": 0.969
},
{
"from": "2022-06-12T13:00:00.000+02:00",
"consumption": 7.612
}
]
}
}
}
}
It must contain data for every hour from the beginning of the month until the last complete hour.
Collect estimate for hour
This is a function node that receives all the live data and estimates the consumption for the rest of the current hour. It sums up the actual consumption from the beginning of the hour until the current time, and adds the estimate for the rest of the hour, giving a total estimate for the hour.
In the beginning of the code, there is a constant ESTIMATION_TIME_MINUTES
that you can use to decide how many minutes that is used to calculate assumed average consumption.
The function keeps a buffer with all readings for the last period of length given with ESTIMATION_TIME_MINUTES
. It uses this buffer to calculate the average consumption for the period. It uses the result and estimates the consumption for the rest of the hour, assuming that the consumption will be the same.
As outputs it sends the following:
Name | Description |
---|---|
accumulatedConsumption | Accumulated consumption the current day |
accumulatedConsumptionLastHour | Accumulated consumption the current hour. |
periodMs | Period the average is calculated for, in milliseconds. It will increase in the beginning, until it reaches ESTIMATION_TIME_MINUTES * 60 * 1000 |
consumptionInPeriod | Consumption in the last periodMs milliseconds. Used as estimate for the remaining of the hour. |
averageConsumptionNow | Consumption the last minute (or ESTIMATION_TIME_MINUTES minutes). |
timeLeftMs | Number of milliseconds left in the current hour. |
consumptionLeft | Estimated consumption the remaining of the hour. |
hourEstimate | The estimated consumption for the total hour. |
currentHour | The time of the current hour. |
Code
Find highest per day
Based on the result from the tibber query, gives the following output:
Name | Description |
---|---|
highestPerDay | The highest hour for each day until now this month, including current day. |
highestCounting | The 3 highest days current month. Can be other than 3 by changing the MAX_COUNTING constant in the beginning of the script. |
highestToday | The highest hour that has ended until now this day. |
currentMonthlyEstimate | The average of the 3 highest days until now this month. That is the capacity that will be used unless it is increased. |
Output is sent when the query is run, that is on startup and when the hour changes.
Code
Calculate values
This is where calculation is done to produce all the output sensor values.
In the beginning of the script there are some constants you can configure:
const HA_NAME = "homeAssistant"; // Your HA name
const STEPS = [2, 5, 10, 15, 20]; // Grid tariff steps in kWh
const MAX_COUNTING = 3; // Number of days to calculate month average of
const BUFFER = 0.5; // kWh - Closer to limit increases alarm level
const SAFE_SONE = 2; // kWh - Further from limit reduces level
const ALARM = 8; // Min level that causes status to be alarm
const MIN_TIMELEFT = 30; //Min level for time left (30 seconds)
The HA_NAME
must be set to the name you have given your Home Assistant. One place to find this is in Node-RED, in the Context Data
window (next to the Debug
window), under Global
, click the refresh button and see the homeassistant
object. Find the name used to the right. In this example the value you are looking for is homeAssistant
:
You must configure the STEPS
array to contain steps relevant for you. You should omit steps you do not plan to go under, to avoid non-necessary actions and warnings.
See Calculated sensor values for description of the output.
Code
Update sensors
This function node maps the values from the previous node to entity_ids that shall be updated in HA. Then, for each sensor, it sends output that uses the API node to perform the actual update.
You can remove sensors that you do not want.
Code
Set entity
Reduction Actions
This is where you set up actions to be taken in case reduction is required or recommended.
Configure actions
You must set up your own actions in this script, if you are going to use actions.
In the On Start tab of this node, you set up the actions by writing a Javascript array, the actions
array. The example shows some actions, but you may set up any number of actions.
There are two types of actions that can be sent:
Call service actions
These actions are taken by sending a payload to a HA call service
node (the Perform action
node). The items in the actions
array contains the payload you need to send to the call service
node in order to take action, and the payload you need to send to the same call service
node in order to reset the action.
Override Power Saver actions
These actions are taken by sending an override message to one or more strategy nodes. Reduction is done by sending override off
, and reset is done by sending override auto
. To use this type of action, specify the name of the strategy node that shall be overridden in the nameOfStrategyToOverride
attribute of the action.
Then send output 2 from the Reduction Actions
node to the input of the strategy node.
You may have multiple actions controlling multiple strategy nodes. They are separated using the name
of the strategy node, so make sure they all have different names. Output 2 must be sent to all strategy nodes that shall be controlled.
If you send the output to two strategy nodes with the same name, they will both be controlled.
You can use this to override the following nodes:
- Best Save
- Lowest Price
- Schedule Merger
- Fixed Schedule
If you are using the schedule merger node, you do not have to override the strategy nodes preceding the schedule merger, only override the schedule merger.
Entity consumption
An action may be to turn on or off a switch, to perform a climate control or what ever else you can do to control your entities.
In order to know how much power that is saved by turning off an action, you should specify this for each action, using the consumption
attribute. This can be used in 3 different ways:
- The entity_id of a sensor that gives the consumption in kW (recommended)
- A number with the consumption in kW
- A function returning the consumption.
Consumption must be kW
If your sensor gives consumption in W, not the required kW, you should find a way to divide it by 1000.
Actions configuration
Each item in the actions
array contains the following data:
Variable name | Description |
---|---|
consumption | The consumption that will be reduced by taking the action, given as either a) (Recommended) The entity_id of a sensor that gives the consumption, or b) A number with the consumption in kWh, or c) a function returning the consumption. |
name | The name of the actions. Can be any thing. |
id | A unique id of the action. |
minAlarmLevel | The minimum alarm level that must be present to take this action. |
reduceWhenRecommended | If true the action will be taken when Reduction Recommended > 0. If false the action will be taken only when Reduction Required > 0 |
minTimeOffSec | The action will not be reset until minimum this number of seconds has passed since the action was taken. |
payloadToTakeAction | The payload that shall be sent to the call service node to take the action (for example turn off a switch). |
payloadToResetAction | The payload that shall be sent to the call service node to reset the action (for example turn a switch back on again). |
nameOfStrategyToOverride | The name of the strategy node that shall be overridden by this action. |
Action type
Use either nameOfStrategyToOverride
or payloadToTakeAction
and payloadToResetAction
. If you use them both at the same time, both actions are performed.
Actions order
Actions to reduce consumption are taken in the order they appear in the actions
array until enough reduction has been done, so put first the actions you want to take first, and last those actions that you want to take only when really necessary.
Here is an example of an actions
array with two items (a water heater and a heating cable):
On Start code
Please note that there is a small piece of code after the actions
array in the On Start
tab. Make sure you do not delete that code.
Sensors without actions
If you don't want the actions, or you want to control actions another way, you can omit the action-related nodes and only use the nodes creating the sensors.
The MIN_MINUTES_INTO_HOUR_TO_TAKE_ACTION
constant in the On Message
code sets a period (of default 5 minutes) in the beginning of the hour when no reduction action is taken. This is to avoid that a high consumption at the end of the previous hour causes reduction actions to be taken as soon as the hour changes.
Code
Reset Actions
This node will reset actions when there is enough capacity available, that is for example turning switches back on again.
In the script, there is a BUFFER_TO_RESET
constant used to set a buffer (in kW) so actions are not reset until there is some spare capacity. By default is it set to 1 kW.
Code
Perform action
This is a call service
node used to perform the call service actions (both taking actions and resetting actions). There is no setup here except selecting the HA server.
Save actions to file
Saves some information to a file, when actions are taken or reset. This is just so you can watch what has been done. Please make sure the file name configured works for you (for example that the folder exists in your HA instance).
Catch action errors
This is supposed to catch any errors in the action-related nodes, and log them to the file.
The code
Below is the code for the Node-RED flow. Copy the code and paste it to Node-RED using Import in the NR menu.
Flow code
[
{
"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"
}
]