node-red-contrib-power-saver 3.6.2 → 4.0.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/.eslintrc.js +15 -0
- package/docs/.vuepress/components/DonateButtons.vue +26 -3
- package/docs/.vuepress/components/VippsPlakat.vue +20 -0
- package/docs/.vuepress/config.js +17 -10
- package/docs/.vuepress/public/ads.txt +1 -0
- package/docs/README.md +4 -4
- package/docs/changelog/README.md +55 -1
- package/docs/contribute/README.md +8 -3
- package/docs/examples/example-grid-tariff-capacity-flow.json +23 -7
- package/docs/examples/example-grid-tariff-capacity-part.md +657 -22
- package/docs/faq/README.md +1 -1
- package/docs/faq/best-save-viewer.md +1 -1
- package/docs/guide/README.md +20 -5
- package/docs/images/best-save-config.png +0 -0
- package/docs/images/combine-two-lowest-price.png +0 -0
- package/docs/images/fixed-schedule-config.png +0 -0
- package/docs/images/global-context-window.png +0 -0
- package/docs/images/lowest-price-config.png +0 -0
- package/docs/images/node-ps-schedule-merger.png +0 -0
- package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
- package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
- package/docs/images/schedule-merger-config.png +0 -0
- package/docs/images/schedule-merger-example-1.png +0 -0
- package/docs/images/vipps-plakat.png +0 -0
- package/docs/images/vipps-qr.png +0 -0
- package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
- package/docs/nodes/README.md +12 -6
- package/docs/nodes/dynamic-commands.md +79 -0
- package/docs/nodes/dynamic-config.md +76 -0
- package/docs/nodes/ps-elvia-add-tariff.md +4 -0
- package/docs/nodes/ps-general-add-tariff.md +10 -0
- package/docs/nodes/ps-receive-price.md +2 -1
- package/docs/nodes/ps-schedule-merger.md +227 -0
- package/docs/nodes/ps-strategy-best-save.md +46 -110
- package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
- package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
- package/docs/nodes/ps-strategy-lowest-price.md +51 -112
- package/package.json +5 -2
- package/src/elvia/elvia-add-tariff.html +1 -2
- package/src/elvia/elvia-add-tariff.js +0 -1
- package/src/elvia/elvia-api.js +6 -0
- package/src/elvia/elvia-tariff.html +1 -1
- package/src/general-add-tariff.html +14 -8
- package/src/general-add-tariff.js +0 -1
- package/src/handle-input.js +94 -106
- package/src/handle-output.js +109 -0
- package/src/receive-price-functions.js +3 -3
- package/src/schedule-merger-functions.js +98 -0
- package/src/schedule-merger.html +135 -0
- package/src/schedule-merger.js +108 -0
- package/src/strategy-best-save.html +38 -1
- package/src/strategy-best-save.js +17 -63
- package/src/strategy-fixed-schedule.html +339 -0
- package/src/strategy-fixed-schedule.js +84 -0
- package/src/strategy-functions.js +35 -0
- package/src/strategy-lowest-price.html +76 -38
- package/src/strategy-lowest-price.js +16 -35
- package/src/utils.js +75 -2
- package/test/commands-input-best-save.test.js +142 -0
- package/test/commands-input-lowest-price.test.js +149 -0
- package/test/commands-input-schedule-merger.test.js +128 -0
- package/test/data/best-save-overlap-result.json +5 -1
- package/test/data/best-save-result.json +4 -0
- package/test/data/commands-result-best-save.json +383 -0
- package/test/data/commands-result-lowest-price.json +340 -0
- package/test/data/fixed-schedule-result.json +353 -0
- package/test/data/lowest-price-result-cont-max-fail.json +5 -1
- package/test/data/lowest-price-result-cont-max.json +3 -1
- package/test/data/lowest-price-result-cont.json +8 -1
- package/test/data/lowest-price-result-missing-end.json +8 -3
- package/test/data/lowest-price-result-neg-cont.json +27 -0
- package/test/data/lowest-price-result-neg-split.json +23 -0
- package/test/data/lowest-price-result-split-allday.json +3 -1
- package/test/data/lowest-price-result-split-allday10.json +1 -0
- package/test/data/lowest-price-result-split-max.json +3 -1
- package/test/data/lowest-price-result-split.json +3 -1
- package/test/data/merge-schedule-data.js +238 -0
- package/test/data/negative-prices.json +197 -0
- package/test/data/nordpool-event-prices.json +96 -480
- package/test/data/nordpool-zero-prices.json +90 -0
- package/test/data/reconfigResult.js +1 -0
- package/test/data/result.js +1 -0
- package/test/data/tibber-result-end-0-24h.json +12 -2
- package/test/data/tibber-result-end-0.json +12 -2
- package/test/data/tibber-result.json +1 -0
- package/test/receive-price.test.js +22 -0
- package/test/schedule-merger-functions.test.js +101 -0
- package/test/schedule-merger-test-utils.js +27 -0
- package/test/schedule-merger.test.js +130 -0
- package/test/send-config-input.test.js +45 -2
- package/test/strategy-best-save-test-utils.js +1 -1
- package/test/strategy-best-save.test.js +45 -0
- package/test/strategy-fixed-schedule.test.js +117 -0
- package/test/strategy-heat-capacitor.test.js +1 -1
- package/test/strategy-lowest-price-functions.test.js +1 -1
- package/test/strategy-lowest-price-test-utils.js +31 -0
- package/test/strategy-lowest-price.test.js +55 -45
- package/test/test-utils.js +43 -36
- package/test/utils.test.js +13 -0
- package/docs/images/node-power-saver.png +0 -0
- package/docs/nodes/power-saver.md +0 -23
- package/src/power-saver.html +0 -116
- package/src/power-saver.js +0 -260
- package/test/commands-input.test.js +0 -47
- package/test/power-saver.test.js +0 -189
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const {
|
|
2
|
+
booleanConfig,
|
|
3
|
+
calcNullSavings,
|
|
4
|
+
fixOutputValues,
|
|
5
|
+
fixPeriods,
|
|
6
|
+
getSavings,
|
|
7
|
+
saveOriginalConfig,
|
|
8
|
+
} = require("./utils");
|
|
9
|
+
const { strategyOnInput } = require("./strategy-functions");
|
|
10
|
+
const { DateTime } = require("luxon");
|
|
11
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
12
|
+
|
|
13
|
+
module.exports = function (RED) {
|
|
14
|
+
function StrategyFixedScheduleNode(config) {
|
|
15
|
+
RED.nodes.createNode(this, config);
|
|
16
|
+
const node = this;
|
|
17
|
+
node.status({});
|
|
18
|
+
|
|
19
|
+
const validConfig = {
|
|
20
|
+
periods: config.periods || [],
|
|
21
|
+
validFrom: config.validFrom,
|
|
22
|
+
validTo: config.validTo,
|
|
23
|
+
days: config.days || { Mon: true, Tue: true, Wed: true, Thu: true, Fri: true, Sat: true, Sun: true },
|
|
24
|
+
contextStorage: config.contextStorage || "default",
|
|
25
|
+
outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule),
|
|
26
|
+
outputValueForOn: config.outputValueForOn || true,
|
|
27
|
+
outputValueForOff: config.outputValueForOff || false,
|
|
28
|
+
outputValueForOntype: config.outputValueForOntype || "bool",
|
|
29
|
+
outputValueForOfftype: config.outputValueForOfftype || "bool",
|
|
30
|
+
override: "auto",
|
|
31
|
+
sendCurrentValueWhenRescheduling: config.sendCurrentValueWhenRescheduling,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
fixOutputValues(validConfig);
|
|
35
|
+
fixPeriods(validConfig);
|
|
36
|
+
saveOriginalConfig(node, validConfig);
|
|
37
|
+
|
|
38
|
+
node.on("close", function () {
|
|
39
|
+
clearTimeout(node.schedulingTimeout);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
node.on("input", function (msg) {
|
|
43
|
+
strategyOnInput(node, msg, doPlanning, calcNullSavings);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
RED.nodes.registerType("ps-strategy-fixed-schedule", StrategyFixedScheduleNode);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function doPlanning(node, priceData) {
|
|
50
|
+
const startTimes = priceData.map((pd) => pd.start);
|
|
51
|
+
const onOff = startTimes.map(() => node.outputIfNoSchedule);
|
|
52
|
+
const allHours = buildAllHours(node, node.periods);
|
|
53
|
+
const validFrom = DateTime.fromISO(node.validFrom || startTimes[0].substr(0, 10));
|
|
54
|
+
const validTo = DateTime.fromISO(node.validTo || startTimes[startTimes.length - 1].substr(0, 10));
|
|
55
|
+
startTimes.forEach((st, i) => {
|
|
56
|
+
const date = DateTime.fromISO(st.substr(0, 10));
|
|
57
|
+
const hour = DateTime.fromISO(st).hour;
|
|
58
|
+
const day = DateTime.fromISO(st).weekday;
|
|
59
|
+
const dayName = Object.keys(node.days)[day - 1];
|
|
60
|
+
if (date >= validFrom && date <= validTo && node.days[dayName]) {
|
|
61
|
+
onOff[i] = allHours[hour];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return onOff;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildAllHours(node, periods) {
|
|
69
|
+
const sortedPeriods = cloneDeep(periods);
|
|
70
|
+
sortedPeriods.sort((a, b) => a.start - b.start);
|
|
71
|
+
let res = [];
|
|
72
|
+
let hour = 0;
|
|
73
|
+
let current = sortedPeriods[sortedPeriods.length - 1];
|
|
74
|
+
sortedPeriods.push({ start: 24, value: null });
|
|
75
|
+
sortedPeriods.forEach((period) => {
|
|
76
|
+
const nextHour = parseInt(period.start);
|
|
77
|
+
while (hour < nextHour) {
|
|
78
|
+
res[hour] = current.value;
|
|
79
|
+
hour++;
|
|
80
|
+
}
|
|
81
|
+
current = period;
|
|
82
|
+
});
|
|
83
|
+
return res;
|
|
84
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { DateTime } = require("luxon");
|
|
2
|
+
const { getEffectiveConfig, getOutputForTime } = require("./utils");
|
|
3
|
+
const { handleStrategyInput } = require("./handle-input");
|
|
4
|
+
const { handleOutput, shallSendOutput, strategyShallSendSchedule } = require("./handle-output");
|
|
5
|
+
|
|
6
|
+
function strategyOnInput(node, msg, doPlanning, calcSavings) {
|
|
7
|
+
if (msg.payload?.name && msg.payload.name !== node.name) {
|
|
8
|
+
// If payload.name is set, and does not match this nodes name, discard message
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const config = getEffectiveConfig(node, msg);
|
|
12
|
+
const { plan, commands } = handleStrategyInput(node, msg, config, doPlanning, calcSavings);
|
|
13
|
+
if (plan) {
|
|
14
|
+
const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
|
|
15
|
+
const currentOutput = node.context().get("currentOutput");
|
|
16
|
+
const plannedOutputNow =
|
|
17
|
+
node.override === "auto"
|
|
18
|
+
? getOutputForTime(plan.schedule, planFromTime, node.outputIfNoSchedule)
|
|
19
|
+
: node.override === "on";
|
|
20
|
+
const outputCommands = {
|
|
21
|
+
sendOutput: shallSendOutput(
|
|
22
|
+
msg,
|
|
23
|
+
commands,
|
|
24
|
+
currentOutput,
|
|
25
|
+
plannedOutputNow,
|
|
26
|
+
node.sendCurrentValueWhenRescheduling
|
|
27
|
+
),
|
|
28
|
+
sendSchedule: strategyShallSendSchedule(msg, commands),
|
|
29
|
+
runSchedule: commands.replan !== false,
|
|
30
|
+
};
|
|
31
|
+
handleOutput(node, config, plan, outputCommands, planFromTime);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { strategyOnInput };
|
|
@@ -61,6 +61,24 @@
|
|
|
61
61
|
required: true,
|
|
62
62
|
align: "left",
|
|
63
63
|
},
|
|
64
|
+
outputValueForOn: {
|
|
65
|
+
value: true,
|
|
66
|
+
required: true,
|
|
67
|
+
validate: RED.validators.typedInput("outputValueForOntype", false),
|
|
68
|
+
},
|
|
69
|
+
outputValueForOff: {
|
|
70
|
+
value: false,
|
|
71
|
+
required: true,
|
|
72
|
+
validate: RED.validators.typedInput("outputValueForOfftype", false),
|
|
73
|
+
},
|
|
74
|
+
outputValueForOntype: {
|
|
75
|
+
value: "bool",
|
|
76
|
+
required: true,
|
|
77
|
+
},
|
|
78
|
+
outputValueForOfftype: {
|
|
79
|
+
value: "bool",
|
|
80
|
+
required: true,
|
|
81
|
+
},
|
|
64
82
|
outputIfNoSchedule: { value: "true", required: true, align: "left" },
|
|
65
83
|
outputOutsidePeriod: { value: "false", required: true, align: "left" },
|
|
66
84
|
contextStorage: { value: "default", required: false, align: "left" },
|
|
@@ -132,58 +150,78 @@
|
|
|
132
150
|
},
|
|
133
151
|
],
|
|
134
152
|
});
|
|
153
|
+
$("#node-input-outputValueForOn").typedInput({
|
|
154
|
+
default: "bool",
|
|
155
|
+
typeField: $("#node-input-outputValueForOntype"),
|
|
156
|
+
types: ["bool", "num", "str"],
|
|
157
|
+
});
|
|
158
|
+
$("#node-input-outputValueForOff").typedInput({
|
|
159
|
+
default: "bool",
|
|
160
|
+
typeField: $("#node-input-outputValueForOfftype"),
|
|
161
|
+
types: ["bool", "num", "str"],
|
|
162
|
+
});
|
|
135
163
|
},
|
|
136
164
|
});
|
|
137
165
|
</script>
|
|
138
166
|
|
|
139
167
|
<script type="text/html" data-template-name="ps-strategy-lowest-price">
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</div>
|
|
144
|
-
<div class="form-row">
|
|
145
|
-
<label for="node-input-fromTime"><i class="fa fa-clock-o"></i> From time</label>
|
|
146
|
-
<input type="text" id="node-input-fromTime" style="width: 80px">
|
|
168
|
+
<div class="form-row">
|
|
169
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
170
|
+
<input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
|
|
147
171
|
</div>
|
|
148
|
-
<div class="form-row">
|
|
149
|
-
<label for="node-input-toTime"><i class="fa fa-clock-o"></i> To time</label>
|
|
150
|
-
<input type="text" id="node-input-toTime" style="width: 80px">
|
|
151
|
-
</div>
|
|
152
|
-
<div class="form-row">
|
|
153
|
-
<label for="node-input-hoursOn"><i class="fa fa-arrows-h"></i> Hours on</label>
|
|
154
|
-
<input type="text" id="node-input-hoursOn" style="width: 80px">
|
|
155
|
-
</div>
|
|
156
|
-
<div class="form-row">
|
|
157
|
-
<label for="node-input-maxPrice"><i class="fa fa-minus"></i> Max price</label>
|
|
158
|
-
<input type="text" id="node-input-maxPrice" placeholder="Max price" style="width: 80px">
|
|
159
|
-
</div>
|
|
160
|
-
<div class="form-row">
|
|
161
|
-
<label for="node-input-doNotSplit">Consecutive on-period</label>
|
|
162
|
-
<input type="checkbox" id="node-input-doNotSplit" style="display:inline-block; width:22px; vertical-align:top;">
|
|
163
|
-
</label>
|
|
164
|
-
</div>
|
|
165
172
|
<div class="form-row">
|
|
166
|
-
<label for="node-input-
|
|
167
|
-
<input type="
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
<label for="node-input-fromTime"><i class="fa fa-clock-o"></i> From time</label>
|
|
174
|
+
<input type="text" id="node-input-fromTime" style="width: 80px">
|
|
175
|
+
</div>
|
|
176
|
+
<div class="form-row">
|
|
177
|
+
<label for="node-input-toTime"><i class="fa fa-clock-o"></i> To time</label>
|
|
178
|
+
<input type="text" id="node-input-toTime" style="width: 80px">
|
|
172
179
|
</div>
|
|
173
180
|
<div class="form-row">
|
|
174
|
-
<label for="node-input-
|
|
175
|
-
<input type="text" id="node-input-
|
|
176
|
-
</label>
|
|
181
|
+
<label for="node-input-hoursOn"><i class="fa fa-arrows-h"></i> Hours on</label>
|
|
182
|
+
<input type="text" id="node-input-hoursOn" style="width: 80px">
|
|
177
183
|
</div>
|
|
178
184
|
<div class="form-row">
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</label>
|
|
185
|
+
<label for="node-input-maxPrice"><i class="fa fa-minus"></i> Max price</label>
|
|
186
|
+
<input type="text" id="node-input-maxPrice" placeholder="Max price" style="width: 80px">
|
|
182
187
|
</div>
|
|
183
188
|
<div class="form-row">
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
<label for="node-input-doNotSplit">Consecutive on-period</label>
|
|
190
|
+
<input type="checkbox" id="node-input-doNotSplit" style="display:inline-block; width:22px; vertical-align:top;">
|
|
191
|
+
</label>
|
|
186
192
|
</div>
|
|
193
|
+
<div class="form-row">
|
|
194
|
+
<label for="node-input-outputValueForOn">Output value for on</label>
|
|
195
|
+
<input type="text" id="node-input-outputValueForOn" style="text-align: left; width: 120px">
|
|
196
|
+
<input type="hidden" id="node-input-outputValueForOntype">
|
|
197
|
+
</div>
|
|
198
|
+
<div class="form-row">
|
|
199
|
+
<label for="node-input-outputValueForOff">Output value for off</label>
|
|
200
|
+
<input type="text" id="node-input-outputValueForOff" style="text-align: left; width: 120px">
|
|
201
|
+
<input type="hidden" id="node-input-outputValueForOfftype">
|
|
202
|
+
</div>
|
|
203
|
+
<div class="form-row">
|
|
204
|
+
<label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
|
|
205
|
+
<input type="checkbox"
|
|
206
|
+
id="node-input-sendCurrentValueWhenRescheduling"
|
|
207
|
+
style="display:inline-block; width:22px; vertical-align:top;"
|
|
208
|
+
autocomplete="off"><span>Send when rescheduling</span>
|
|
209
|
+
</label>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="form-row">
|
|
212
|
+
<label for="node-input-outputIfNoSchedule">If no schedule, send</label>
|
|
213
|
+
<input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
|
|
214
|
+
</label>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="form-row">
|
|
217
|
+
<label for="node-input-outputIfNoSchedule">Outside period, send</label>
|
|
218
|
+
<input type="text" id="node-input-outputOutsidePeriod" style="width: 80px">
|
|
219
|
+
</label>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="form-row">
|
|
222
|
+
<label for="node-input-contextStorage"><i class="fa fa-archive"></i> Context storage</label>
|
|
223
|
+
<input type="text" id="node-input-contextStorage" style="width: 160px">
|
|
224
|
+
</div>
|
|
187
225
|
</script>
|
|
188
226
|
|
|
189
227
|
<script type="text/markdown" data-help-name="ps-strategy-lowest-price">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { DateTime } = require("luxon");
|
|
2
|
-
const { booleanConfig,
|
|
3
|
-
const { handleStrategyInput } = require("./handle-input");
|
|
2
|
+
const { booleanConfig, calcNullSavings, fixOutputValues, saveOriginalConfig } = require("./utils");
|
|
4
3
|
const { getBestContinuous, getBestX } = require("./strategy-lowest-price-functions");
|
|
4
|
+
const { strategyOnInput } = require("./strategy-functions");
|
|
5
5
|
|
|
6
6
|
module.exports = function (RED) {
|
|
7
7
|
function StrategyLowestPriceNode(config) {
|
|
@@ -9,7 +9,7 @@ module.exports = function (RED) {
|
|
|
9
9
|
const node = this;
|
|
10
10
|
node.status({});
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const validConfig = {
|
|
13
13
|
fromTime: config.fromTime,
|
|
14
14
|
toTime: config.toTime,
|
|
15
15
|
hoursOn: parseInt(config.hoursOn),
|
|
@@ -18,27 +18,31 @@ module.exports = function (RED) {
|
|
|
18
18
|
sendCurrentValueWhenRescheduling: booleanConfig(config.sendCurrentValueWhenRescheduling),
|
|
19
19
|
outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule),
|
|
20
20
|
outputOutsidePeriod: booleanConfig(config.outputOutsidePeriod),
|
|
21
|
+
outputValueForOn: config.outputValueForOn || true,
|
|
22
|
+
outputValueForOff: config.outputValueForOff || false,
|
|
23
|
+
outputValueForOntype: config.outputValueForOntype || "bool",
|
|
24
|
+
outputValueForOfftype: config.outputValueForOfftype || "bool",
|
|
25
|
+
override: "auto",
|
|
21
26
|
contextStorage: config.contextStorage || "default",
|
|
22
27
|
};
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
fixOutputValues(validConfig);
|
|
30
|
+
saveOriginalConfig(node, validConfig);
|
|
25
31
|
|
|
26
32
|
node.on("close", function () {
|
|
27
33
|
clearTimeout(node.schedulingTimeout);
|
|
28
34
|
});
|
|
29
35
|
|
|
30
36
|
node.on("input", function (msg) {
|
|
31
|
-
|
|
37
|
+
strategyOnInput(node, msg, doPlanning, calcNullSavings);
|
|
32
38
|
});
|
|
33
39
|
}
|
|
34
|
-
|
|
35
40
|
RED.nodes.registerType("ps-strategy-lowest-price", StrategyLowestPriceNode);
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
function doPlanning(node,
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const startTimes = [...dataDayBefore.hours.map((h) => h.start), ...priceData.map((pd) => pd.start)];
|
|
43
|
+
function doPlanning(node, priceData) {
|
|
44
|
+
const values = priceData.map((pd) => pd.value);
|
|
45
|
+
const startTimes = priceData.map((pd) => pd.start);
|
|
42
46
|
|
|
43
47
|
const from = parseInt(node.fromTime);
|
|
44
48
|
const to = parseInt(node.toTime);
|
|
@@ -78,18 +82,6 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
78
82
|
|
|
79
83
|
const onOff = [];
|
|
80
84
|
|
|
81
|
-
// Fill in data from previous plan for StartMissing
|
|
82
|
-
const lastStartMissing = periodStatus.lastIndexOf((s) => s === "StartMissing");
|
|
83
|
-
if (lastStartMissing >= 0 && dataDayBefore?.hours?.length > 0) {
|
|
84
|
-
const lastBefore = DateTime.fromISO(dataDayBefore.hours[dataDayBefore.hours.length - 1].start);
|
|
85
|
-
if (lastBefore >= DateTime.fromISO(startTimes[lastStartMissing])) {
|
|
86
|
-
for (let i = 0; i <= lastStartMissing; i++) {
|
|
87
|
-
onOff[i] = dataDayBefore.hours.find((h) => h.start === startTimes[i]);
|
|
88
|
-
periodStatus[i] = "Backfilled";
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
85
|
// Set onOff for hours that will not be planned
|
|
94
86
|
periodStatus.forEach((s, i) => {
|
|
95
87
|
onOff[i] =
|
|
@@ -104,18 +96,7 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
104
96
|
makePlan(node, values, onOff, s, endIndexes[i]);
|
|
105
97
|
});
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const hours = values.map((v, i) => ({
|
|
110
|
-
price: v,
|
|
111
|
-
onOff: onOff[i],
|
|
112
|
-
start: startTimes[i],
|
|
113
|
-
saving: null,
|
|
114
|
-
}));
|
|
115
|
-
return {
|
|
116
|
-
hours,
|
|
117
|
-
schedule,
|
|
118
|
-
};
|
|
99
|
+
return onOff;
|
|
119
100
|
}
|
|
120
101
|
|
|
121
102
|
function makePlan(node, values, onOff, fromIndex, toIndex) {
|
package/src/utils.js
CHANGED
|
@@ -4,6 +4,21 @@ function booleanConfig(value) {
|
|
|
4
4
|
return value === "true" || value === true;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function calcNullSavings(values, _) {
|
|
8
|
+
return values.map(() => null);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Save the config object in the context, and set
|
|
13
|
+
* all values directly on the node.
|
|
14
|
+
*
|
|
15
|
+
* @param {*} node
|
|
16
|
+
* @param {*} originalConfig Object with config values
|
|
17
|
+
*/
|
|
18
|
+
function saveOriginalConfig(node, originalConfig) {
|
|
19
|
+
node.context().set("config", originalConfig);
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/**
|
|
8
23
|
* Sort values in array and return array with index of original array
|
|
9
24
|
* in sorted order. Highest value first.
|
|
@@ -62,14 +77,22 @@ function getEffectiveConfig(node, msg) {
|
|
|
62
77
|
node.error("Node has no config");
|
|
63
78
|
return {};
|
|
64
79
|
}
|
|
80
|
+
res.hasChanged = false;
|
|
65
81
|
const isConfigMsg = !!msg?.payload?.config;
|
|
66
82
|
if (isConfigMsg) {
|
|
67
83
|
const inputConfig = msg.payload.config;
|
|
68
84
|
Object.keys(inputConfig).forEach((key) => {
|
|
69
|
-
res[key]
|
|
85
|
+
if (res[key] !== inputConfig[key]) {
|
|
86
|
+
res[key] = inputConfig[key];
|
|
87
|
+
res.hasChanged = true;
|
|
88
|
+
}
|
|
70
89
|
});
|
|
71
90
|
node.context().set("config", res);
|
|
72
91
|
}
|
|
92
|
+
|
|
93
|
+
// Store config variables in node
|
|
94
|
+
Object.keys(res).forEach((key) => (node[key] = res[key]));
|
|
95
|
+
|
|
73
96
|
return res;
|
|
74
97
|
}
|
|
75
98
|
|
|
@@ -77,7 +100,7 @@ function loadDayData(node, date) {
|
|
|
77
100
|
// Load saved schedule for the date (YYYY-MM-DD)
|
|
78
101
|
// Return null if not found
|
|
79
102
|
const key = date.toISODate();
|
|
80
|
-
const saved = node.context().get(key
|
|
103
|
+
const saved = node.context().get(key);
|
|
81
104
|
const res = saved ?? {
|
|
82
105
|
schedule: [],
|
|
83
106
|
hours: [],
|
|
@@ -152,6 +175,14 @@ function makeSchedule(onOff, startTimes, initial = null) {
|
|
|
152
175
|
return res;
|
|
153
176
|
}
|
|
154
177
|
|
|
178
|
+
function makeScheduleFromHours(hours, initial = null) {
|
|
179
|
+
return makeSchedule(
|
|
180
|
+
hours.map((h) => h.onOff),
|
|
181
|
+
hours.map((h) => h.start),
|
|
182
|
+
initial
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
155
186
|
function fillArray(value, count) {
|
|
156
187
|
if (value === undefined || count <= 0) {
|
|
157
188
|
return [];
|
|
@@ -187,21 +218,63 @@ function validationFailure(node, message, status = null) {
|
|
|
187
218
|
node.warn(message);
|
|
188
219
|
}
|
|
189
220
|
|
|
221
|
+
function msgHasPriceData(msg) {
|
|
222
|
+
return !!msg?.payload?.priceData;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function msgHasConfig(msg) {
|
|
226
|
+
return !!msg?.payload?.config;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fixOutputValues(config) {
|
|
230
|
+
if (config.outputValueForOntype === "bool") {
|
|
231
|
+
config.outputValueForOn = booleanConfig(config.outputValueForOn);
|
|
232
|
+
}
|
|
233
|
+
if (config.outputValueForOntype === "num") {
|
|
234
|
+
config.outputValueForOn = Number(config.outputValueForOn);
|
|
235
|
+
}
|
|
236
|
+
if (config.outputValueForOfftype === "bool") {
|
|
237
|
+
config.outputValueForOff = booleanConfig(config.outputValueForOff);
|
|
238
|
+
}
|
|
239
|
+
if (config.outputValueForOfftype === "num") {
|
|
240
|
+
config.outputValueForOff = Number(config.outputValueForOff);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function fixPeriods(config) {
|
|
245
|
+
config.periods.forEach((p) => {
|
|
246
|
+
p.value = p.value === "true" || p.value === true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getOutputForTime(schedule, time, defaultValue) {
|
|
251
|
+
const pastSchedule = schedule.filter((entry) => DateTime.fromISO(entry.time) <= time);
|
|
252
|
+
return pastSchedule.length ? pastSchedule[pastSchedule.length - 1].value : defaultValue;
|
|
253
|
+
}
|
|
254
|
+
|
|
190
255
|
module.exports = {
|
|
191
256
|
booleanConfig,
|
|
257
|
+
calcNullSavings,
|
|
192
258
|
countAtEnd,
|
|
193
259
|
extractPlanForDate,
|
|
194
260
|
fillArray,
|
|
195
261
|
firstOn,
|
|
262
|
+
fixOutputValues,
|
|
263
|
+
fixPeriods,
|
|
196
264
|
getDiff,
|
|
197
265
|
getDiffToNextOn,
|
|
198
266
|
getEffectiveConfig,
|
|
267
|
+
getOutputForTime,
|
|
199
268
|
getSavings,
|
|
200
269
|
getStartAtIndex,
|
|
201
270
|
isSameDate,
|
|
202
271
|
loadDayData,
|
|
203
272
|
makeSchedule,
|
|
273
|
+
makeScheduleFromHours,
|
|
274
|
+
msgHasConfig,
|
|
275
|
+
msgHasPriceData,
|
|
204
276
|
roundPrice,
|
|
277
|
+
saveOriginalConfig,
|
|
205
278
|
sortedIndex,
|
|
206
279
|
validationFailure,
|
|
207
280
|
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const expect = require("expect");
|
|
2
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
3
|
+
const helper = require("node-red-node-test-helper");
|
|
4
|
+
const bestSave = require("../src/strategy-best-save.js");
|
|
5
|
+
const prices = require("./data/converted-prices.json");
|
|
6
|
+
const result = require("./data/commands-result-best-save.json");
|
|
7
|
+
const { equalPlan } = require("./test-utils");
|
|
8
|
+
const { makeFlow } = require("./strategy-best-save-test-utils");
|
|
9
|
+
|
|
10
|
+
helper.init(require.resolve("node-red"));
|
|
11
|
+
|
|
12
|
+
describe("send command as input to best save", () => {
|
|
13
|
+
beforeEach(function (done) {
|
|
14
|
+
helper.startServer(done);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(function (done) {
|
|
18
|
+
helper.unload().then(function () {
|
|
19
|
+
helper.stopServer(done);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should send schedule on command", function (done) {
|
|
24
|
+
const flow = makeFlow(3, 2, true);
|
|
25
|
+
let pass = 1;
|
|
26
|
+
helper.load(bestSave, flow, function () {
|
|
27
|
+
const n1 = helper.getNode("n1");
|
|
28
|
+
const n2 = helper.getNode("n2");
|
|
29
|
+
n1.sendCurrentValueWhenRescheduling = true;
|
|
30
|
+
n2.on("input", function (msg) {
|
|
31
|
+
switch (pass) {
|
|
32
|
+
case 1:
|
|
33
|
+
pass++;
|
|
34
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
35
|
+
n1.receive({ payload: { commands: { sendSchedule: true } } });
|
|
36
|
+
break;
|
|
37
|
+
case 2:
|
|
38
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
39
|
+
done();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const payload = cloneDeep(prices);
|
|
44
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
45
|
+
n1.receive({ payload });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should send output on command", function (done) {
|
|
50
|
+
const flow = makeFlow(3, 2, true);
|
|
51
|
+
helper.load(bestSave, flow, function () {
|
|
52
|
+
const n1 = helper.getNode("n1");
|
|
53
|
+
const n2 = helper.getNode("n2");
|
|
54
|
+
const n3 = helper.getNode("n3");
|
|
55
|
+
const n4 = helper.getNode("n4");
|
|
56
|
+
n1.sendCurrentValueWhenRescheduling = true;
|
|
57
|
+
let countOn = 0;
|
|
58
|
+
let countOff = 0;
|
|
59
|
+
n2.on("input", function (msg) {
|
|
60
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
61
|
+
n1.receive({ payload: { commands: { sendOutput: true }, time: "2021-10-11T11:00:05.000+02:00" } });
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
console.log("countOn = " + countOn + ", countOff = " + countOff);
|
|
64
|
+
expect(countOn).toEqual(1);
|
|
65
|
+
expect(countOff).toEqual(1);
|
|
66
|
+
done();
|
|
67
|
+
}, 50);
|
|
68
|
+
});
|
|
69
|
+
n3.on("input", function (msg) {
|
|
70
|
+
countOn++;
|
|
71
|
+
expect(msg).toHaveProperty("payload", true);
|
|
72
|
+
});
|
|
73
|
+
n4.on("input", function (msg) {
|
|
74
|
+
countOff++;
|
|
75
|
+
expect(msg).toHaveProperty("payload", false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const payload = cloneDeep(prices);
|
|
79
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
80
|
+
|
|
81
|
+
n1.receive({ payload });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it("should reset on command", function (done) {
|
|
85
|
+
const flow = makeFlow(3, 2, true);
|
|
86
|
+
helper.load(bestSave, flow, function () {
|
|
87
|
+
const n1 = helper.getNode("n1");
|
|
88
|
+
const n2 = helper.getNode("n2");
|
|
89
|
+
n2.on("input", function (msg) {
|
|
90
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
91
|
+
n1.receive({ payload: { commands: { reset: true } } });
|
|
92
|
+
n1.warn.should.be.calledWithExactly("No price data");
|
|
93
|
+
done();
|
|
94
|
+
});
|
|
95
|
+
const payload = cloneDeep(prices);
|
|
96
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
97
|
+
n1.receive({ payload });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should replan on command", function (done) {
|
|
102
|
+
const flow = makeFlow(3, 2, true);
|
|
103
|
+
let pass = 1;
|
|
104
|
+
helper.load(bestSave, flow, function () {
|
|
105
|
+
const n1 = helper.getNode("n1");
|
|
106
|
+
const n2 = helper.getNode("n2");
|
|
107
|
+
const n3 = helper.getNode("n3");
|
|
108
|
+
const n4 = helper.getNode("n4");
|
|
109
|
+
let countOn = 0;
|
|
110
|
+
let countOff = 0;
|
|
111
|
+
n2.on("input", function (msg) {
|
|
112
|
+
switch (pass) {
|
|
113
|
+
case 1:
|
|
114
|
+
pass++;
|
|
115
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
116
|
+
n1.receive({ payload: { commands: { replan: true }, time: "2021-10-11T00:00:05.000+02:00" } });
|
|
117
|
+
break;
|
|
118
|
+
case 2:
|
|
119
|
+
pass++;
|
|
120
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
console.log("countOn = " + countOn + ", countOff = " + countOff);
|
|
123
|
+
expect(countOn).toEqual(0);
|
|
124
|
+
expect(countOff).toEqual(2);
|
|
125
|
+
done();
|
|
126
|
+
}, 50);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
n3.on("input", function (msg) {
|
|
130
|
+
countOn++;
|
|
131
|
+
expect(msg).toHaveProperty("payload", true);
|
|
132
|
+
});
|
|
133
|
+
n4.on("input", function (msg) {
|
|
134
|
+
countOff++;
|
|
135
|
+
expect(msg).toHaveProperty("payload", false);
|
|
136
|
+
});
|
|
137
|
+
const payload = cloneDeep(prices);
|
|
138
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
139
|
+
n1.receive({ payload });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|