node-red-contrib-power-saver 3.6.1 → 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 +18 -10
- package/docs/.vuepress/public/ads.txt +1 -0
- package/docs/README.md +4 -4
- package/docs/changelog/README.md +59 -1
- package/docs/contribute/README.md +8 -3
- package/docs/examples/README.md +2 -0
- package/docs/examples/example-grid-tariff-capacity-flow.json +1142 -0
- package/docs/examples/example-grid-tariff-capacity-part.md +988 -107
- 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/example-capacity-flow.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 +1 -3
- package/src/elvia/elvia-api.js +9 -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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const expect = require("expect");
|
|
2
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
3
|
+
const helper = require("node-red-node-test-helper");
|
|
4
|
+
const lowestPrice = require("../src/strategy-lowest-price.js");
|
|
5
|
+
const prices = require("./data/converted-prices.json");
|
|
6
|
+
const result = require("./data/commands-result-lowest-price.json");
|
|
7
|
+
const { equalPlan, equalSchedule } = require("./test-utils");
|
|
8
|
+
const { makeFlow } = require("./strategy-lowest-price-test-utils");
|
|
9
|
+
|
|
10
|
+
helper.init(require.resolve("node-red"));
|
|
11
|
+
|
|
12
|
+
describe("send command as input to lowest price", () => {
|
|
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);
|
|
25
|
+
let pass = 1;
|
|
26
|
+
helper.load(lowestPrice, 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(equalSchedule(result.schedule, msg.payload.schedule)).toBeTruthy();
|
|
35
|
+
n1.receive({ payload: { commands: { sendSchedule: true } } });
|
|
36
|
+
break;
|
|
37
|
+
case 2:
|
|
38
|
+
expect(equalSchedule(result.schedule, msg.payload.schedule)).toBeTruthy();
|
|
39
|
+
done();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const payload = cloneDeep(prices);
|
|
44
|
+
payload.time = "2021-10-10T00:00:00.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
|
+
let pass = 1;
|
|
52
|
+
helper.load(lowestPrice, flow, function () {
|
|
53
|
+
const n1 = helper.getNode("n1");
|
|
54
|
+
const n2 = helper.getNode("n2");
|
|
55
|
+
const n3 = helper.getNode("n3");
|
|
56
|
+
const n4 = helper.getNode("n4");
|
|
57
|
+
n1.sendCurrentValueWhenRescheduling = true;
|
|
58
|
+
let countOn = 0;
|
|
59
|
+
let countOff = 0;
|
|
60
|
+
n2.on("input", function (msg) {
|
|
61
|
+
switch (pass) {
|
|
62
|
+
case 1:
|
|
63
|
+
pass++;
|
|
64
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
65
|
+
n1.receive({ payload: { commands: { sendOutput: true }, time: "2021-10-11T11:00:05.000+02:00" } });
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
console.log("countOn = " + countOn + ", countOff = " + countOff);
|
|
68
|
+
expect(countOn).toEqual(1);
|
|
69
|
+
expect(countOff).toEqual(1);
|
|
70
|
+
done();
|
|
71
|
+
}, 50);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
n3.on("input", function (msg) {
|
|
76
|
+
countOn++;
|
|
77
|
+
expect(msg).toHaveProperty("payload", true);
|
|
78
|
+
});
|
|
79
|
+
n4.on("input", function (msg) {
|
|
80
|
+
countOff++;
|
|
81
|
+
expect(msg).toHaveProperty("payload", false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const payload = cloneDeep(prices);
|
|
85
|
+
payload.time = "2021-10-10T00:00:05.000+02:00";
|
|
86
|
+
payload.commands = { runSchedule: false };
|
|
87
|
+
|
|
88
|
+
n1.receive({ payload });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it("should reset on command", function (done) {
|
|
92
|
+
const flow = makeFlow(3, 2, true);
|
|
93
|
+
helper.load(lowestPrice, flow, function () {
|
|
94
|
+
const n1 = helper.getNode("n1");
|
|
95
|
+
const n2 = helper.getNode("n2");
|
|
96
|
+
n2.on("input", function (msg) {
|
|
97
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
98
|
+
n1.receive({ payload: { commands: { reset: true } } });
|
|
99
|
+
n1.warn.should.be.calledWithExactly("No price data");
|
|
100
|
+
done();
|
|
101
|
+
});
|
|
102
|
+
const payload = cloneDeep(prices);
|
|
103
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
104
|
+
n1.receive({ payload });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should replan on command", function (done) {
|
|
109
|
+
const flow = makeFlow(3, 2, true);
|
|
110
|
+
let pass = 1;
|
|
111
|
+
helper.load(lowestPrice, flow, function () {
|
|
112
|
+
const n1 = helper.getNode("n1");
|
|
113
|
+
const n2 = helper.getNode("n2");
|
|
114
|
+
const n3 = helper.getNode("n3");
|
|
115
|
+
const n4 = helper.getNode("n4");
|
|
116
|
+
let countOn = 0;
|
|
117
|
+
let countOff = 0;
|
|
118
|
+
n2.on("input", function (msg) {
|
|
119
|
+
switch (pass) {
|
|
120
|
+
case 1:
|
|
121
|
+
pass++;
|
|
122
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
123
|
+
n1.receive({ payload: { commands: { replan: true }, time: "2021-10-11T00:00:05.000+02:00" } });
|
|
124
|
+
break;
|
|
125
|
+
case 2:
|
|
126
|
+
pass++;
|
|
127
|
+
expect(equalPlan(result, msg.payload)).toBeTruthy();
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
console.log("countOn = " + countOn + ", countOff = " + countOff);
|
|
130
|
+
expect(countOn).toEqual(0);
|
|
131
|
+
expect(countOff).toEqual(2);
|
|
132
|
+
done();
|
|
133
|
+
}, 50);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
n3.on("input", function (msg) {
|
|
137
|
+
countOn++;
|
|
138
|
+
expect(msg).toHaveProperty("payload", true);
|
|
139
|
+
});
|
|
140
|
+
n4.on("input", function (msg) {
|
|
141
|
+
countOff++;
|
|
142
|
+
expect(msg).toHaveProperty("payload", false);
|
|
143
|
+
});
|
|
144
|
+
const payload = cloneDeep(prices);
|
|
145
|
+
payload.time = "2021-10-11T00:00:05.000+02:00";
|
|
146
|
+
n1.receive({ payload });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const expect = require("expect");
|
|
2
|
+
const cloneDeep = require("lodash.clonedeep");
|
|
3
|
+
const helper = require("node-red-node-test-helper");
|
|
4
|
+
const prices = require("./data/converted-prices.json");
|
|
5
|
+
const { testPlan, equalHours } = require("./test-utils");
|
|
6
|
+
const { makeFlow, makePayload } = require("./schedule-merger-test-utils");
|
|
7
|
+
const scheduleMerger = require("../src/schedule-merger.js");
|
|
8
|
+
const { allOff, someOn } = require("./data/merge-schedule-data.js");
|
|
9
|
+
|
|
10
|
+
helper.init(require.resolve("node-red"));
|
|
11
|
+
|
|
12
|
+
describe("send command as input to schedule merger", () => {
|
|
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("OR");
|
|
25
|
+
let pass = 1;
|
|
26
|
+
helper.load(scheduleMerger, flow, function () {
|
|
27
|
+
const n1 = helper.getNode("n1");
|
|
28
|
+
const n2 = helper.getNode("n2");
|
|
29
|
+
n2.on("input", function (msg) {
|
|
30
|
+
switch (pass) {
|
|
31
|
+
case 1:
|
|
32
|
+
pass++;
|
|
33
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
34
|
+
n1.warn.should.not.be.called;
|
|
35
|
+
n1.receive({ payload: { commands: { sendSchedule: true } } });
|
|
36
|
+
break;
|
|
37
|
+
case 2:
|
|
38
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
39
|
+
done();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
n1.receive({ payload: makePayload("s1", someOn) });
|
|
44
|
+
n1.receive({ payload: makePayload("s2", allOff) });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should send output on command", function (done) {
|
|
49
|
+
const flow = makeFlow("OR");
|
|
50
|
+
let pass = 1;
|
|
51
|
+
helper.load(scheduleMerger, 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
|
+
let countOn = 0;
|
|
57
|
+
let countOff = 0;
|
|
58
|
+
n2.on("input", function (msg) {
|
|
59
|
+
switch (pass) {
|
|
60
|
+
case 1:
|
|
61
|
+
pass++;
|
|
62
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
63
|
+
n1.warn.should.not.be.called;
|
|
64
|
+
n1.receive({ payload: { commands: { sendOutput: true }, time: "2021-06-20T01:05:00.000+02:00" } });
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
console.log("countOn = " + countOn + ", countOff = " + countOff);
|
|
67
|
+
expect(countOn).toEqual(1);
|
|
68
|
+
expect(countOff).toEqual(1);
|
|
69
|
+
done();
|
|
70
|
+
}, 50);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
n3.on("input", function (msg) {
|
|
75
|
+
countOn++;
|
|
76
|
+
expect(msg).toHaveProperty("payload", true);
|
|
77
|
+
});
|
|
78
|
+
n4.on("input", function (msg) {
|
|
79
|
+
countOff++;
|
|
80
|
+
expect(msg).toHaveProperty("payload", false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
n1.receive({ payload: makePayload("s1", someOn) });
|
|
84
|
+
n1.receive({ payload: makePayload("s2", allOff) });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("should reset on command", function (done) {
|
|
88
|
+
const flow = makeFlow("OR");
|
|
89
|
+
helper.load(scheduleMerger, flow, function () {
|
|
90
|
+
const n1 = helper.getNode("n1");
|
|
91
|
+
const n2 = helper.getNode("n2");
|
|
92
|
+
n2.on("input", function (msg) {
|
|
93
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
94
|
+
n1.warn.should.not.be.called;
|
|
95
|
+
n1.receive({ payload: { commands: { reset: true } } });
|
|
96
|
+
n1.warn.should.be.calledWithExactly("No schedule");
|
|
97
|
+
done();
|
|
98
|
+
});
|
|
99
|
+
n1.receive({ payload: makePayload("s1", someOn) });
|
|
100
|
+
n1.receive({ payload: makePayload("s2", allOff) });
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should replan on command", function (done) {
|
|
105
|
+
const flow = makeFlow("OR");
|
|
106
|
+
let pass = 1;
|
|
107
|
+
helper.load(scheduleMerger, flow, function () {
|
|
108
|
+
const n1 = helper.getNode("n1");
|
|
109
|
+
const n2 = helper.getNode("n2");
|
|
110
|
+
n2.on("input", function (msg) {
|
|
111
|
+
switch (pass) {
|
|
112
|
+
case 1:
|
|
113
|
+
pass++;
|
|
114
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
115
|
+
n1.warn.should.not.be.called;
|
|
116
|
+
n1.receive({ payload: { commands: { replan: true }, time: "2021-06-19T00:00:00.000+02:00" } });
|
|
117
|
+
break;
|
|
118
|
+
case 2:
|
|
119
|
+
expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
|
|
120
|
+
done();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
n1.receive({ payload: makePayload("s1", someOn) });
|
|
125
|
+
n1.receive({ payload: makePayload("s2", allOff) });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
"time": "2022-08-17T23:00:00.000+02:00",
|
|
50
50
|
"value": true,
|
|
51
51
|
"countHours": 1
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"time": "2022-08-18T00:00:00.000+02:00",
|
|
55
|
+
"value": false,
|
|
56
|
+
"countHours": null
|
|
52
57
|
}
|
|
53
58
|
],
|
|
54
59
|
"hours": [
|
|
@@ -350,7 +355,6 @@
|
|
|
350
355
|
"outputIfNoSchedule": false,
|
|
351
356
|
"contextStorage": "default"
|
|
352
357
|
},
|
|
353
|
-
"sentOnCommand": false,
|
|
354
358
|
"time": "2022-08-16T17:14:35.673+02:00",
|
|
355
359
|
"version": "3.6.0",
|
|
356
360
|
"current": false
|