node-red-contrib-power-saver 3.0.10 → 3.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/docs/.vuepress/config.js +5 -1
- package/docs/.vuepress/dist/404.html +3 -3
- package/docs/.vuepress/dist/assets/img/add-tariff-flow.eb700d4f.png +0 -0
- package/docs/.vuepress/dist/assets/img/next-schedule-entity.4406856a.png +0 -0
- package/docs/.vuepress/dist/assets/img/next-schedule-flow.413ad62b.png +0 -0
- package/docs/.vuepress/dist/assets/img/next-schedule-sensor.eb896bdd.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-general-add-tariff.a3cf6f06.png +0 -0
- package/docs/.vuepress/dist/assets/js/app.eae70176.js +1 -0
- package/docs/.vuepress/dist/assets/js/runtime~app.3384c251.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0607240a.661e1808.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-08683c60.a6b9cf5b.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0aca7ba6.b42fad7f.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.d62b30f7.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1ad821fa.6e2194d0.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1e2b191e.50b8fa18.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-30acb564.f2fcd69f.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-4637f9e4.b67738ed.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-510ed0d4.528dd6f3.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-5954bcb2.182daf70.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-5db8da3a.f2de6cb9.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-61f728ca.6fdbbb92.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-677dfaed.0013f083.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-7446a652.d05e2648.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-7c87f26e.1127dcf5.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-8daa1a0e.dde202c9.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-b4a42144.9e5f9728.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-e8c55052.8384b053.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-fffb8e28.3406fd88.js +1 -0
- package/docs/.vuepress/dist/changelog/index.html +3 -3
- package/docs/.vuepress/dist/contribute/index.html +3 -3
- package/docs/.vuepress/dist/examples/example-next-schedule-entity.html +25 -0
- package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +4 -4
- package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +4 -4
- package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +4 -4
- package/docs/.vuepress/dist/examples/index.html +3 -3
- package/docs/.vuepress/dist/faq/index.html +15 -0
- package/docs/.vuepress/dist/guide/index.html +4 -4
- package/docs/.vuepress/dist/index.html +3 -3
- package/docs/.vuepress/dist/nodes/index.html +3 -3
- package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +4 -4
- package/docs/.vuepress/dist/nodes/power-saver.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-general-add-tariff.html +15 -0
- package/docs/.vuepress/dist/nodes/ps-receive-price.html +4 -4
- package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +7 -5
- package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +7 -5
- package/docs/.vuepress/dist/nodes/strategy-input.html +4 -4
- package/docs/README.md +2 -2
- package/docs/changelog/README.md +19 -0
- package/docs/examples/README.md +4 -0
- package/docs/examples/example-next-schedule-entity.md +41 -0
- package/docs/faq/README.md +23 -0
- package/docs/guide/README.md +4 -5
- package/docs/images/add-tariff-flow.png +0 -0
- package/docs/images/mysterious-plan.png +0 -0
- package/docs/images/next-schedule-entity.png +0 -0
- package/docs/images/next-schedule-flow.png +0 -0
- package/docs/images/next-schedule-sensor.png +0 -0
- package/docs/images/node-ps-general-add-tariff.png +0 -0
- package/docs/nodes/README.md +6 -0
- package/docs/nodes/ps-elvia-add-tariff.md +1 -1
- package/docs/nodes/ps-general-add-tariff.md +51 -0
- package/docs/nodes/ps-strategy-best-save.md +6 -2
- package/docs/nodes/ps-strategy-lowest-price.md +6 -2
- package/package.json +3 -2
- package/src/elvia/elvia-add-tariff.js +5 -1
- package/src/general-add-tariff-functions.js +46 -0
- package/src/general-add-tariff.html +186 -0
- package/src/general-add-tariff.js +35 -0
- package/src/handle-input.js +14 -5
- package/src/power-saver.js +1 -1
- package/src/receive-price.js +6 -1
- package/src/strategy-lowest-price.js +6 -6
- package/test/data/nordpool-3-days-prices.json +293 -0
- package/test/data/nordpool-3-days-result.json +444 -0
- package/test/data/tibber-result-end-0-24h.json +3 -1
- package/test/data/tibber-result-end-0.json +3 -1
- package/test/general-add-tariff-functions.test.js +104 -0
- package/test/general-add-tariff.test.js +186 -0
- package/test/send-config-input.test.js +44 -0
- package/test/strategy-lowest-price-3days.test.js +88 -0
- package/test/strategy-lowest-price.test.js +5 -0
- package/docs/.vuepress/dist/assets/js/app.3cfedce6.js +0 -1
- package/docs/.vuepress/dist/assets/js/runtime~app.f1d7fab8.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-08683c60.07fe8291.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-0aca7ba6.aec5ba75.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.163b80fb.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-1ad821fa.85407071.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-30acb564.73b8e29f.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-4637f9e4.60300b77.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-510ed0d4.b76b84de.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-5954bcb2.9e6d2df1.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-5db8da3a.ac192f35.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-61f728ca.64fa763c.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-677dfaed.b84f09f5.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-7c87f26e.91c245da.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-8daa1a0e.66c9dbce.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-b4a42144.d1856a24.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-e8c55052.5f85b6cd.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-fffb8e28.9e0579a1.js +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ps-general-add-tariff
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Node to add a value, for example a variable grid tariff, to the price before it is used to calculate savings in the strategy nodes.
|
|
6
|
+
|
|
7
|
+
## Description
|
|
8
|
+
|
|
9
|
+
This node is useful if there is an addition to the electricity price that varies over the day, as it might be for the grid tariff.
|
|
10
|
+
|
|
11
|
+
If there is one price for example from 22:00 to 06:00 every day, and another price from 06:00 to 22:00, this is the right node to use. It can be used for more than two periods, as long as the time it changes is the same every day.
|
|
12
|
+
|
|
13
|
+
Here is how this node is normally used:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
::: tip Changes during the year
|
|
18
|
+
If there is one price now, and another price from a specific date, you can use two nodes after each other. Set the `Valid to date` of the node with the current prices to the last date the current prices are valid. Set the `Valid from date` of the node with the upcoming prices to the first date those prices are valid.
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
### Add and delete periods
|
|
24
|
+
|
|
25
|
+
You can have from 1 to 24 periods during the day, with different values to add for each hour. Click the `Add period` button to add more periods. Click the `X` button to delete a period.
|
|
26
|
+
|
|
27
|
+
### From time and Value
|
|
28
|
+
|
|
29
|
+
For each period, select the time of the day the value is valid from, and enter the value.
|
|
30
|
+
|
|
31
|
+
### Valid from date
|
|
32
|
+
|
|
33
|
+
Fill in the first date the config is valid.
|
|
34
|
+
|
|
35
|
+
If this is empty, the config is valid from the dawn of time.
|
|
36
|
+
|
|
37
|
+
### Valid to date
|
|
38
|
+
|
|
39
|
+
Fill in the last date the config is valid.
|
|
40
|
+
|
|
41
|
+
If this is empty, the config is valid until forever.
|
|
42
|
+
|
|
43
|
+
## Input
|
|
44
|
+
|
|
45
|
+
The input is the [common strategy input format](./strategy-input.md)
|
|
46
|
+
|
|
47
|
+
## Output
|
|
48
|
+
|
|
49
|
+
The output is the [common strategy input format](./strategy-input.md)
|
|
50
|
+
|
|
51
|
+
If there is a config property in the input payload, it is passed on to the output payload.
|
|
@@ -52,7 +52,9 @@ All the variables in the config object are optional. You can send only those you
|
|
|
52
52
|
|
|
53
53
|
The config sent like this will be valid until a new config is sent the same way, or until the flow is restarted. On a restart, the original config set up in the node will be used.
|
|
54
54
|
|
|
55
|
-
When a config is sent like this, the schedule will be replanned based on the last previously received price data. If no price data has been received, no scheduling is done.
|
|
55
|
+
When a config is sent like this, and without price data, the schedule will be replanned based on the last previously received price data. If no price data has been received, no scheduling is done.
|
|
56
|
+
|
|
57
|
+
However, you can send config and price data in the same message. Then both will be used .
|
|
56
58
|
|
|
57
59
|
## Input
|
|
58
60
|
|
|
@@ -115,7 +117,9 @@ Example of output:
|
|
|
115
117
|
"minSaving": 0.001,
|
|
116
118
|
"sendCurrentValueWhenRescheduling": true,
|
|
117
119
|
"outputIfNoSchedule": false
|
|
118
|
-
}
|
|
120
|
+
},
|
|
121
|
+
"time": "2021-09-30T23:45:12.123+02:00",
|
|
122
|
+
"version": "3.1.2"
|
|
119
123
|
}
|
|
120
124
|
```
|
|
121
125
|
|
|
@@ -70,7 +70,9 @@ All the variables in the config object are optional. You can send only those you
|
|
|
70
70
|
|
|
71
71
|
The config sent like this will be valid until a new config is sent the same way, or until the flow is restarted. On a restart, the original config set up in the node will be used.
|
|
72
72
|
|
|
73
|
-
When a config is sent like this, the schedule will be replanned based on the last previously received price data. If no price data has been received, no scheduling is done.
|
|
73
|
+
When a config is sent like this, and without price data, the schedule will be replanned based on the last previously received price data. If no price data has been received, no scheduling is done.
|
|
74
|
+
|
|
75
|
+
However, you can send config and price data in the same message. Then both will be used .
|
|
74
76
|
|
|
75
77
|
## Input
|
|
76
78
|
|
|
@@ -144,7 +146,9 @@ Example of output:
|
|
|
144
146
|
"sendCurrentValueWhenRescheduling": true,
|
|
145
147
|
"outputIfNoSchedule": "true",
|
|
146
148
|
"outputOutsidePeriod": "true"
|
|
147
|
-
}
|
|
149
|
+
},
|
|
150
|
+
"time": "2021-09-30T23:45:12.123+02:00",
|
|
151
|
+
"version": "3.1.2"
|
|
148
152
|
}
|
|
149
153
|
```
|
|
150
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-power-saver",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A module for Node-RED that you can use to turn on and off a switch based on power prices",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"ps-elvia-config": "src/elvia/elvia-config.js",
|
|
29
29
|
"ps-elvia-tariff-types": "src/elvia/elvia-tariff-types.js",
|
|
30
30
|
"ps-elvia-tariff": "src/elvia/elvia-tariff.js",
|
|
31
|
-
"ps-elvia-add-tariff": "src/elvia/elvia-add-tariff.js"
|
|
31
|
+
"ps-elvia-add-tariff": "src/elvia/elvia-add-tariff.js",
|
|
32
|
+
"ps-general-add-tariff": "src/general-add-tariff.js"
|
|
32
33
|
}
|
|
33
34
|
},
|
|
34
35
|
"prettier": {
|
|
@@ -39,7 +39,11 @@ module.exports = function (RED) {
|
|
|
39
39
|
p.value = roundPrice(p.powerPrice + p.gridTariffVariable);
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
const payload = { priceData: prices };
|
|
43
|
+
if (msg.payload.config) {
|
|
44
|
+
payload.config = msg.payload.config;
|
|
45
|
+
}
|
|
46
|
+
node.send([{ payload }]);
|
|
43
47
|
});
|
|
44
48
|
});
|
|
45
49
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
2
|
+
const { DateTime } = require("luxon");
|
|
3
|
+
const { nodes } = require("node-red");
|
|
4
|
+
const { roundPrice } = require("./utils");
|
|
5
|
+
|
|
6
|
+
function buildAllHours(node, periods) {
|
|
7
|
+
const sortedPeriods = cloneDeep(periods);
|
|
8
|
+
sortedPeriods.sort((a, b) => a.start - b.start);
|
|
9
|
+
let res = [];
|
|
10
|
+
let hour = 0;
|
|
11
|
+
let current = sortedPeriods[sortedPeriods.length - 1];
|
|
12
|
+
sortedPeriods.push({ start: 24, value: null });
|
|
13
|
+
sortedPeriods.forEach((period) => {
|
|
14
|
+
const nextHour = parseInt(period.start);
|
|
15
|
+
while (hour < nextHour) {
|
|
16
|
+
let value = 0;
|
|
17
|
+
try {
|
|
18
|
+
value = parseFloat(("" + current.value).replace(",", "."));
|
|
19
|
+
} catch (e) {
|
|
20
|
+
node.warn("Illegal number: " + current.value);
|
|
21
|
+
}
|
|
22
|
+
res[hour] = value;
|
|
23
|
+
hour++;
|
|
24
|
+
}
|
|
25
|
+
current = period;
|
|
26
|
+
});
|
|
27
|
+
return res;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function addTariffToPrices(node, config, prices) {
|
|
31
|
+
const allHours = buildAllHours(node, config.periods);
|
|
32
|
+
const validFrom = DateTime.fromISO(config.validFrom || prices[0].start.substr(0, 10));
|
|
33
|
+
const validTo = DateTime.fromISO(config.validTo || prices[prices.length - 1].start.substr(0, 10));
|
|
34
|
+
prices.forEach((p, i) => {
|
|
35
|
+
const date = DateTime.fromISO(p.start.substr(0, 10));
|
|
36
|
+
const hour = DateTime.fromISO(p.start).hour;
|
|
37
|
+
if (date >= validFrom && date <= validTo) {
|
|
38
|
+
prices[i].value = roundPrice(prices[i].value + allHours[hour]);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
addTariffToPrices,
|
|
45
|
+
buildAllHours,
|
|
46
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
const dateRe = /^$|^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$/;
|
|
3
|
+
const priceRe = /^(\d+\.\d*)|(\d+)$/;
|
|
4
|
+
RED.nodes.registerType("ps-general-add-tariff", {
|
|
5
|
+
category: "Power Saver",
|
|
6
|
+
color: "#a6bbcf",
|
|
7
|
+
defaults: {
|
|
8
|
+
name: { value: "Add General Tariff" },
|
|
9
|
+
periods: {
|
|
10
|
+
value: [
|
|
11
|
+
{ start: "22", value: 0.0 },
|
|
12
|
+
{ start: "06", value: 0.0 },
|
|
13
|
+
],
|
|
14
|
+
validate: function () {
|
|
15
|
+
return !this.periods.some((p) => !priceRe.test("" + p.value));
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
validFrom: { value: null, required: false, validate: RED.validators.regex(dateRe) },
|
|
19
|
+
validTo: { value: null, required: false, validate: RED.validators.regex(dateRe) },
|
|
20
|
+
},
|
|
21
|
+
hours: [
|
|
22
|
+
"00",
|
|
23
|
+
"01",
|
|
24
|
+
"02",
|
|
25
|
+
"03",
|
|
26
|
+
"04",
|
|
27
|
+
"05",
|
|
28
|
+
"06",
|
|
29
|
+
"07",
|
|
30
|
+
"08",
|
|
31
|
+
"09",
|
|
32
|
+
"10",
|
|
33
|
+
"11",
|
|
34
|
+
"12",
|
|
35
|
+
"13",
|
|
36
|
+
"14",
|
|
37
|
+
"15",
|
|
38
|
+
"16",
|
|
39
|
+
"17",
|
|
40
|
+
"18",
|
|
41
|
+
"19",
|
|
42
|
+
"20",
|
|
43
|
+
"21",
|
|
44
|
+
"22",
|
|
45
|
+
"23",
|
|
46
|
+
],
|
|
47
|
+
inputs: 1,
|
|
48
|
+
outputs: 1,
|
|
49
|
+
periodCont: 2,
|
|
50
|
+
icon: "font-awesome/fa-plus",
|
|
51
|
+
color: "#FFCC66",
|
|
52
|
+
label: function () {
|
|
53
|
+
return this.name || "Add Tariff";
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
oneditprepare: function () {
|
|
57
|
+
const createElement = function (type, attrs = [], children = []) {
|
|
58
|
+
const el = document.createElement(type);
|
|
59
|
+
attrs.forEach((attr) => {
|
|
60
|
+
el.setAttribute(attr[0], attr[1]);
|
|
61
|
+
});
|
|
62
|
+
children.forEach((child) => {
|
|
63
|
+
el.append(child);
|
|
64
|
+
});
|
|
65
|
+
return el;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const createInputPart = function (name, i, text, inpStyle, value) {
|
|
69
|
+
const id = `node-input-${name}-${i}`;
|
|
70
|
+
const label = createElement(
|
|
71
|
+
"label",
|
|
72
|
+
[
|
|
73
|
+
["for", id],
|
|
74
|
+
["style", "margin-right: 10px;"],
|
|
75
|
+
],
|
|
76
|
+
[]
|
|
77
|
+
);
|
|
78
|
+
label.innerHTML = text;
|
|
79
|
+
const inp = createElement("input", [
|
|
80
|
+
["type", "text"],
|
|
81
|
+
["id", id],
|
|
82
|
+
["style", `width: 80px; ${inpStyle};`],
|
|
83
|
+
]);
|
|
84
|
+
inp.value = value;
|
|
85
|
+
return createElement("span", [["style", "text-align: right;"]], [label, inp]);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const addPeriod = function (periods) {
|
|
89
|
+
const prev = periods[periods.length - 1].start;
|
|
90
|
+
const next = prev === "23" ? "00" : "" + (parseInt(prev) + 1);
|
|
91
|
+
periods.push({ start: next, value: null });
|
|
92
|
+
drawPeriods(periods);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const removePeriod = function (periods, i) {
|
|
96
|
+
periods.splice(i, 1);
|
|
97
|
+
drawPeriods(periods);
|
|
98
|
+
RED.nodes.dirty(true);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const drawPeriods = function (periods) {
|
|
102
|
+
document.getElementById("node-input-period-container").replaceChildren();
|
|
103
|
+
for (let i = 0; i < periods.length; i++) {
|
|
104
|
+
let period = periods[i];
|
|
105
|
+
|
|
106
|
+
const timeEl = createInputPart("fromTime", i, "From time:", "margin-right: 20px;", period.start);
|
|
107
|
+
const valEl = createInputPart("value", i, "Value:", "margin-right: 20px;", period.value);
|
|
108
|
+
|
|
109
|
+
let li;
|
|
110
|
+
if (periods.length > 1) {
|
|
111
|
+
// Delete button
|
|
112
|
+
const delButton = document.createElement("button");
|
|
113
|
+
delButton.setAttribute("style", "width: 24px;");
|
|
114
|
+
delButton.innerText = "X";
|
|
115
|
+
delButton.addEventListener("click", () => {
|
|
116
|
+
removePeriod(periods, i);
|
|
117
|
+
});
|
|
118
|
+
li = createElement("div", [["style", "text-align: left;"]], [timeEl, valEl, delButton]);
|
|
119
|
+
} else {
|
|
120
|
+
li = createElement("div", [["style", "text-align: left;"]], [timeEl, valEl]);
|
|
121
|
+
}
|
|
122
|
+
$("#node-input-period-container").append(li);
|
|
123
|
+
|
|
124
|
+
$("#node-input-fromTime-" + i).typedInput({
|
|
125
|
+
types: [
|
|
126
|
+
{
|
|
127
|
+
value: "fromtime",
|
|
128
|
+
options: hours.map((h) => ({ value: h, label: h + ":00" })),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
$("#node-input-fromTime-" + i).change(function () {
|
|
133
|
+
periods[i].start = this.value;
|
|
134
|
+
RED.nodes.dirty(true);
|
|
135
|
+
});
|
|
136
|
+
$("#node-input-value-" + i).change(function () {
|
|
137
|
+
periods[i].value = this.value;
|
|
138
|
+
RED.nodes.dirty(true);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
drawPeriods(this.periods);
|
|
144
|
+
$("#add-period-button").on("click", () => {
|
|
145
|
+
addPeriod(this.periods);
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<script type="text/html" data-template-name="ps-general-add-tariff">
|
|
152
|
+
<div class="form-row">
|
|
153
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
154
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="form-row node-input-period-container-row">
|
|
158
|
+
<div id="node-input-period-container"></div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="form-row">
|
|
162
|
+
<button type="button" id="add-period-button" class="red-ui-button">Add period</button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<h3>Optional:</h3>
|
|
166
|
+
|
|
167
|
+
<div class="form-row">
|
|
168
|
+
<label for="node-input-validFrom"><i class="fa fa-calendar"></i> Valid from date</label>
|
|
169
|
+
<input type="text" id="node-input-validFrom" placeholder="YYYY-MM-DD" />
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="form-row">
|
|
173
|
+
<label for="node-input-validTo"><i class="fa fa-calendar"></i> Valid to date</label>
|
|
174
|
+
<input type="text" id="node-input-validTo" placeholder="YYYY-MM-DD" />
|
|
175
|
+
</div>
|
|
176
|
+
</script>
|
|
177
|
+
|
|
178
|
+
<script type="text/markdown" data-help-name="ps-general-add-tariff">
|
|
179
|
+
# Add Tariff
|
|
180
|
+
|
|
181
|
+
A node to add a tariff that is fixed for periods of the day/year
|
|
182
|
+
to the price before it is sent to the strategy nodes.
|
|
183
|
+
Use this node between the receive-price node and any of the strategy nodes.
|
|
184
|
+
|
|
185
|
+
Please read more in the [node documentation](https://ottopaulsen.github.io/node-red-contrib-power-saver/nodes/ps-general-add-tariff)
|
|
186
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
2
|
+
const { DateTime } = require("luxon");
|
|
3
|
+
const { addTariffToPrices, buildAllHours } = require("./general-add-tariff-functions");
|
|
4
|
+
const { roundPrice } = require("./utils");
|
|
5
|
+
const { extractPlanForDate, getEffectiveConfig, validationFailure } = require("./utils");
|
|
6
|
+
|
|
7
|
+
module.exports = function (RED) {
|
|
8
|
+
function PsGeneralAddTariffNode(config) {
|
|
9
|
+
RED.nodes.createNode(this, config);
|
|
10
|
+
this.range = config.range;
|
|
11
|
+
const node = this;
|
|
12
|
+
|
|
13
|
+
const originalConfig = {
|
|
14
|
+
periods: config.periods,
|
|
15
|
+
validFrom: config.validFrom,
|
|
16
|
+
validTo: config.validTo,
|
|
17
|
+
};
|
|
18
|
+
node.context().set("config", originalConfig);
|
|
19
|
+
|
|
20
|
+
node.on("input", function (originalMessage) {
|
|
21
|
+
const msg = cloneDeep(originalMessage);
|
|
22
|
+
const effectiveConfig = getEffectiveConfig(node, msg);
|
|
23
|
+
const prices = msg.payload.priceData;
|
|
24
|
+
if (!prices || prices.length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addTariffToPrices(node, effectiveConfig, prices);
|
|
29
|
+
|
|
30
|
+
node.send(msg);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
RED.nodes.registerType("ps-general-add-tariff", PsGeneralAddTariffNode);
|
|
35
|
+
};
|
package/src/handle-input.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
const { extractPlanForDate, getEffectiveConfig, validationFailure } = require("./utils");
|
|
2
2
|
const { DateTime } = require("luxon");
|
|
3
|
+
const { version } = require("../package.json");
|
|
3
4
|
|
|
4
5
|
function handleStrategyInput(node, msg, doPlanning) {
|
|
5
6
|
node.schedulingTimeout = null;
|
|
6
7
|
|
|
7
8
|
const effectiveConfig = getEffectiveConfig(node, msg);
|
|
9
|
+
|
|
8
10
|
if (!validateInput(node, msg)) {
|
|
9
11
|
return;
|
|
10
12
|
}
|
|
11
13
|
const priceData = getPriceData(node, msg);
|
|
14
|
+
if (!priceData) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
12
17
|
const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
|
|
13
18
|
|
|
14
19
|
// Store config variables in node
|
|
@@ -38,6 +43,8 @@ function handleStrategyInput(node, msg, doPlanning) {
|
|
|
38
43
|
hours: plan.hours,
|
|
39
44
|
source: msg.payload.source,
|
|
40
45
|
config: effectiveConfig,
|
|
46
|
+
time: planFromTime.toISO(),
|
|
47
|
+
version,
|
|
41
48
|
},
|
|
42
49
|
};
|
|
43
50
|
|
|
@@ -45,11 +52,12 @@ function handleStrategyInput(node, msg, doPlanning) {
|
|
|
45
52
|
const pastSchedule = plan.schedule.filter((entry) => DateTime.fromISO(entry.time) <= planFromTime);
|
|
46
53
|
|
|
47
54
|
const sendNow = node.sendCurrentValueWhenRescheduling && pastSchedule.length > 0;
|
|
55
|
+
const currentValue = pastSchedule[pastSchedule.length - 1]?.value;
|
|
48
56
|
if (sendNow) {
|
|
49
|
-
const currentValue = pastSchedule[pastSchedule.length - 1].value;
|
|
50
57
|
output1 = currentValue ? { payload: true } : null;
|
|
51
58
|
output2 = currentValue ? null : { payload: false };
|
|
52
59
|
}
|
|
60
|
+
output3.payload.current = currentValue;
|
|
53
61
|
|
|
54
62
|
// Delete old data
|
|
55
63
|
deleteSavedScheduleBefore(node, dateDayBefore);
|
|
@@ -63,7 +71,8 @@ function handleStrategyInput(node, msg, doPlanning) {
|
|
|
63
71
|
|
|
64
72
|
function getPriceData(node, msg) {
|
|
65
73
|
const isConfigMsg = !!msg?.payload?.config;
|
|
66
|
-
|
|
74
|
+
const isPriceMsg = !!msg?.payload?.priceData;
|
|
75
|
+
if (isConfigMsg && !isPriceMsg) {
|
|
67
76
|
return node.context().get("lastPriceData");
|
|
68
77
|
}
|
|
69
78
|
const priceData = msg.payload.priceData;
|
|
@@ -82,9 +91,9 @@ function runSchedule(node, schedule, time, currentSent = false) {
|
|
|
82
91
|
const wait = nextTime - currentTime;
|
|
83
92
|
const onOff = entry.value ? "on" : "off";
|
|
84
93
|
node.log("Switching " + onOff + " in " + wait + " milliseconds");
|
|
85
|
-
const statusMessage =
|
|
94
|
+
const statusMessage = `${remainingSchedule.length} changes - ${
|
|
86
95
|
remainingSchedule[0].value ? "on" : "off"
|
|
87
|
-
}`;
|
|
96
|
+
} at ${nextTime.toLocaleString(DateTime.TIME_SIMPLE)}`;
|
|
88
97
|
node.status({ fill: "green", shape: "dot", text: statusMessage });
|
|
89
98
|
return setTimeout(() => {
|
|
90
99
|
sendSwitch(node, entry.value);
|
|
@@ -104,7 +113,7 @@ function deleteSavedScheduleBefore(node, day) {
|
|
|
104
113
|
let date = day;
|
|
105
114
|
do {
|
|
106
115
|
date = date.plus({ days: -1 });
|
|
107
|
-
data = node.context().
|
|
116
|
+
data = node.context().set(date.toISO(), undefined);
|
|
108
117
|
} while (data);
|
|
109
118
|
}
|
|
110
119
|
|
package/src/power-saver.js
CHANGED
package/src/receive-price.js
CHANGED
|
@@ -12,8 +12,13 @@ module.exports = function (RED) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const payload = { priceData, source };
|
|
16
|
+
if (msg.config) {
|
|
17
|
+
payload.config = msg.config;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
// Send output
|
|
16
|
-
node.send({ payload
|
|
21
|
+
node.send({ payload });
|
|
17
22
|
});
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -43,8 +43,8 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
43
43
|
const endIndexes = [];
|
|
44
44
|
let currentStatus = from < (to === 0 && to !== from ? 24 : to) ? "Outside" : "StartMissing";
|
|
45
45
|
let hour;
|
|
46
|
-
|
|
47
|
-
hour = DateTime.fromISO(
|
|
46
|
+
startTimes.forEach((st, i) => {
|
|
47
|
+
hour = DateTime.fromISO(st).hour;
|
|
48
48
|
if (hour === to && to === from && currentStatus === "Inside") {
|
|
49
49
|
endIndexes.push(i - 1);
|
|
50
50
|
}
|
|
@@ -63,13 +63,13 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
63
63
|
let i = periodStatus.length - 1;
|
|
64
64
|
do {
|
|
65
65
|
periodStatus[i] = "EndMissing";
|
|
66
|
-
hour = DateTime.fromISO(
|
|
66
|
+
hour = DateTime.fromISO(startTimes[i]).hour;
|
|
67
67
|
i--;
|
|
68
68
|
} while (periodStatus[i] === "Inside" && hour !== from);
|
|
69
69
|
startIndexes.splice(startIndexes.length - 1, 1);
|
|
70
70
|
}
|
|
71
71
|
if (hour === (to === 0 ? 23 : to - 1)) {
|
|
72
|
-
endIndexes.push(
|
|
72
|
+
endIndexes.push(startTimes.length - 1);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const onOff = [];
|
|
@@ -78,9 +78,9 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
78
78
|
const lastStartMissing = periodStatus.lastIndexOf((s) => s === "StartMissing");
|
|
79
79
|
if (lastStartMissing >= 0 && dataDayBefore?.hours?.length > 0) {
|
|
80
80
|
const lastBefore = DateTime.fromISO(dataDayBefore.hours[dataDayBefore.hours.length - 1].start);
|
|
81
|
-
if (lastBefore >= DateTime.fromISO(
|
|
81
|
+
if (lastBefore >= DateTime.fromISO(startTimes[lastStartMissing])) {
|
|
82
82
|
for (let i = 0; i <= lastStartMissing; i++) {
|
|
83
|
-
onOff[i] = dataDayBefore.hours.find((h) => h.start ===
|
|
83
|
+
onOff[i] = dataDayBefore.hours.find((h) => h.start === startTimes[i]);
|
|
84
84
|
periodStatus[i] = "Backfilled";
|
|
85
85
|
}
|
|
86
86
|
}
|