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.
Files changed (100) hide show
  1. package/docs/.vuepress/config.js +5 -1
  2. package/docs/.vuepress/dist/404.html +3 -3
  3. package/docs/.vuepress/dist/assets/img/add-tariff-flow.eb700d4f.png +0 -0
  4. package/docs/.vuepress/dist/assets/img/next-schedule-entity.4406856a.png +0 -0
  5. package/docs/.vuepress/dist/assets/img/next-schedule-flow.413ad62b.png +0 -0
  6. package/docs/.vuepress/dist/assets/img/next-schedule-sensor.eb896bdd.png +0 -0
  7. package/docs/.vuepress/dist/assets/img/node-ps-general-add-tariff.a3cf6f06.png +0 -0
  8. package/docs/.vuepress/dist/assets/js/app.eae70176.js +1 -0
  9. package/docs/.vuepress/dist/assets/js/runtime~app.3384c251.js +1 -0
  10. package/docs/.vuepress/dist/assets/js/v-0607240a.661e1808.js +1 -0
  11. package/docs/.vuepress/dist/assets/js/v-08683c60.a6b9cf5b.js +1 -0
  12. package/docs/.vuepress/dist/assets/js/v-0aca7ba6.b42fad7f.js +1 -0
  13. package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.d62b30f7.js +1 -0
  14. package/docs/.vuepress/dist/assets/js/v-1ad821fa.6e2194d0.js +1 -0
  15. package/docs/.vuepress/dist/assets/js/v-1e2b191e.50b8fa18.js +1 -0
  16. package/docs/.vuepress/dist/assets/js/v-30acb564.f2fcd69f.js +1 -0
  17. package/docs/.vuepress/dist/assets/js/v-4637f9e4.b67738ed.js +1 -0
  18. package/docs/.vuepress/dist/assets/js/v-510ed0d4.528dd6f3.js +1 -0
  19. package/docs/.vuepress/dist/assets/js/v-5954bcb2.182daf70.js +1 -0
  20. package/docs/.vuepress/dist/assets/js/v-5db8da3a.f2de6cb9.js +1 -0
  21. package/docs/.vuepress/dist/assets/js/v-61f728ca.6fdbbb92.js +1 -0
  22. package/docs/.vuepress/dist/assets/js/v-677dfaed.0013f083.js +1 -0
  23. package/docs/.vuepress/dist/assets/js/v-7446a652.d05e2648.js +1 -0
  24. package/docs/.vuepress/dist/assets/js/v-7c87f26e.1127dcf5.js +1 -0
  25. package/docs/.vuepress/dist/assets/js/v-8daa1a0e.dde202c9.js +1 -0
  26. package/docs/.vuepress/dist/assets/js/v-b4a42144.9e5f9728.js +1 -0
  27. package/docs/.vuepress/dist/assets/js/v-e8c55052.8384b053.js +1 -0
  28. package/docs/.vuepress/dist/assets/js/v-fffb8e28.3406fd88.js +1 -0
  29. package/docs/.vuepress/dist/changelog/index.html +3 -3
  30. package/docs/.vuepress/dist/contribute/index.html +3 -3
  31. package/docs/.vuepress/dist/examples/example-next-schedule-entity.html +25 -0
  32. package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +4 -4
  33. package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +4 -4
  34. package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +4 -4
  35. package/docs/.vuepress/dist/examples/index.html +3 -3
  36. package/docs/.vuepress/dist/faq/index.html +15 -0
  37. package/docs/.vuepress/dist/guide/index.html +4 -4
  38. package/docs/.vuepress/dist/index.html +3 -3
  39. package/docs/.vuepress/dist/nodes/index.html +3 -3
  40. package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +4 -4
  41. package/docs/.vuepress/dist/nodes/power-saver.html +3 -3
  42. package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +3 -3
  43. package/docs/.vuepress/dist/nodes/ps-general-add-tariff.html +15 -0
  44. package/docs/.vuepress/dist/nodes/ps-receive-price.html +4 -4
  45. package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +7 -5
  46. package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +7 -5
  47. package/docs/.vuepress/dist/nodes/strategy-input.html +4 -4
  48. package/docs/README.md +2 -2
  49. package/docs/changelog/README.md +19 -0
  50. package/docs/examples/README.md +4 -0
  51. package/docs/examples/example-next-schedule-entity.md +41 -0
  52. package/docs/faq/README.md +23 -0
  53. package/docs/guide/README.md +4 -5
  54. package/docs/images/add-tariff-flow.png +0 -0
  55. package/docs/images/mysterious-plan.png +0 -0
  56. package/docs/images/next-schedule-entity.png +0 -0
  57. package/docs/images/next-schedule-flow.png +0 -0
  58. package/docs/images/next-schedule-sensor.png +0 -0
  59. package/docs/images/node-ps-general-add-tariff.png +0 -0
  60. package/docs/nodes/README.md +6 -0
  61. package/docs/nodes/ps-elvia-add-tariff.md +1 -1
  62. package/docs/nodes/ps-general-add-tariff.md +51 -0
  63. package/docs/nodes/ps-strategy-best-save.md +6 -2
  64. package/docs/nodes/ps-strategy-lowest-price.md +6 -2
  65. package/package.json +3 -2
  66. package/src/elvia/elvia-add-tariff.js +5 -1
  67. package/src/general-add-tariff-functions.js +46 -0
  68. package/src/general-add-tariff.html +186 -0
  69. package/src/general-add-tariff.js +35 -0
  70. package/src/handle-input.js +14 -5
  71. package/src/power-saver.js +1 -1
  72. package/src/receive-price.js +6 -1
  73. package/src/strategy-lowest-price.js +6 -6
  74. package/test/data/nordpool-3-days-prices.json +293 -0
  75. package/test/data/nordpool-3-days-result.json +444 -0
  76. package/test/data/tibber-result-end-0-24h.json +3 -1
  77. package/test/data/tibber-result-end-0.json +3 -1
  78. package/test/general-add-tariff-functions.test.js +104 -0
  79. package/test/general-add-tariff.test.js +186 -0
  80. package/test/send-config-input.test.js +44 -0
  81. package/test/strategy-lowest-price-3days.test.js +88 -0
  82. package/test/strategy-lowest-price.test.js +5 -0
  83. package/docs/.vuepress/dist/assets/js/app.3cfedce6.js +0 -1
  84. package/docs/.vuepress/dist/assets/js/runtime~app.f1d7fab8.js +0 -1
  85. package/docs/.vuepress/dist/assets/js/v-08683c60.07fe8291.js +0 -1
  86. package/docs/.vuepress/dist/assets/js/v-0aca7ba6.aec5ba75.js +0 -1
  87. package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.163b80fb.js +0 -1
  88. package/docs/.vuepress/dist/assets/js/v-1ad821fa.85407071.js +0 -1
  89. package/docs/.vuepress/dist/assets/js/v-30acb564.73b8e29f.js +0 -1
  90. package/docs/.vuepress/dist/assets/js/v-4637f9e4.60300b77.js +0 -1
  91. package/docs/.vuepress/dist/assets/js/v-510ed0d4.b76b84de.js +0 -1
  92. package/docs/.vuepress/dist/assets/js/v-5954bcb2.9e6d2df1.js +0 -1
  93. package/docs/.vuepress/dist/assets/js/v-5db8da3a.ac192f35.js +0 -1
  94. package/docs/.vuepress/dist/assets/js/v-61f728ca.64fa763c.js +0 -1
  95. package/docs/.vuepress/dist/assets/js/v-677dfaed.b84f09f5.js +0 -1
  96. package/docs/.vuepress/dist/assets/js/v-7c87f26e.91c245da.js +0 -1
  97. package/docs/.vuepress/dist/assets/js/v-8daa1a0e.66c9dbce.js +0 -1
  98. package/docs/.vuepress/dist/assets/js/v-b4a42144.d1856a24.js +0 -1
  99. package/docs/.vuepress/dist/assets/js/v-e8c55052.5f85b6cd.js +0 -1
  100. package/docs/.vuepress/dist/assets/js/v-fffb8e28.9e0579a1.js +0 -1
@@ -49,4 +49,4 @@ The input is the [common strategy input format](./strategy-input.md)
49
49
 
50
50
  ## Output
51
51
 
52
- The input is the [common strategy input format](./strategy-input.md)
52
+ The output is the [common strategy input format](./strategy-input.md)
@@ -0,0 +1,51 @@
1
+ # ps-general-add-tariff
2
+
3
+ ![ps-general-add-tariff](../images/node-ps-general-add-tariff.png)
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
+ ![general flow](../images/add-tariff-flow.png)
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.10",
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
- node.send([{ payload: { priceData: prices } }]);
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
+ };
@@ -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
- if (isConfigMsg) {
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 = `Scheduled ${remainingSchedule.length} changes. Next: ${
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().get(date.toISO());
116
+ data = node.context().set(date.toISO(), undefined);
108
117
  } while (data);
109
118
  }
110
119
 
@@ -156,7 +156,7 @@ function deleteSavedScheduleBefore(node, day) {
156
156
  let date = day;
157
157
  do {
158
158
  date = date.plus({ days: -1 });
159
- data = node.context().get(date.toISO());
159
+ data = node.context().set(date.toISO(), undefined);
160
160
  } while (data);
161
161
  }
162
162
 
@@ -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: { priceData, source } });
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
- priceData.forEach((pd, i) => {
47
- hour = DateTime.fromISO(pd.start).hour;
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(priceData[i].start).hour;
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(priceData.length - 1);
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(priceData[lastStartMissing].start)) {
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 === priceData[i].start);
83
+ onOff[i] = dataDayBefore.hours.find((h) => h.start === startTimes[i]);
84
84
  periodStatus[i] = "Backfilled";
85
85
  }
86
86
  }