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.
Files changed (107) hide show
  1. package/.eslintrc.js +15 -0
  2. package/docs/.vuepress/components/DonateButtons.vue +26 -3
  3. package/docs/.vuepress/components/VippsPlakat.vue +20 -0
  4. package/docs/.vuepress/config.js +18 -10
  5. package/docs/.vuepress/public/ads.txt +1 -0
  6. package/docs/README.md +4 -4
  7. package/docs/changelog/README.md +59 -1
  8. package/docs/contribute/README.md +8 -3
  9. package/docs/examples/README.md +2 -0
  10. package/docs/examples/example-grid-tariff-capacity-flow.json +1142 -0
  11. package/docs/examples/example-grid-tariff-capacity-part.md +988 -107
  12. package/docs/faq/README.md +1 -1
  13. package/docs/faq/best-save-viewer.md +1 -1
  14. package/docs/guide/README.md +20 -5
  15. package/docs/images/best-save-config.png +0 -0
  16. package/docs/images/combine-two-lowest-price.png +0 -0
  17. package/docs/images/example-capacity-flow.png +0 -0
  18. package/docs/images/fixed-schedule-config.png +0 -0
  19. package/docs/images/global-context-window.png +0 -0
  20. package/docs/images/lowest-price-config.png +0 -0
  21. package/docs/images/node-ps-schedule-merger.png +0 -0
  22. package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
  23. package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
  24. package/docs/images/schedule-merger-config.png +0 -0
  25. package/docs/images/schedule-merger-example-1.png +0 -0
  26. package/docs/images/vipps-plakat.png +0 -0
  27. package/docs/images/vipps-qr.png +0 -0
  28. package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
  29. package/docs/nodes/README.md +12 -6
  30. package/docs/nodes/dynamic-commands.md +79 -0
  31. package/docs/nodes/dynamic-config.md +76 -0
  32. package/docs/nodes/ps-elvia-add-tariff.md +4 -0
  33. package/docs/nodes/ps-general-add-tariff.md +10 -0
  34. package/docs/nodes/ps-receive-price.md +2 -1
  35. package/docs/nodes/ps-schedule-merger.md +227 -0
  36. package/docs/nodes/ps-strategy-best-save.md +46 -110
  37. package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
  38. package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
  39. package/docs/nodes/ps-strategy-lowest-price.md +51 -112
  40. package/package.json +5 -2
  41. package/src/elvia/elvia-add-tariff.html +1 -2
  42. package/src/elvia/elvia-add-tariff.js +1 -3
  43. package/src/elvia/elvia-api.js +9 -0
  44. package/src/elvia/elvia-tariff.html +1 -1
  45. package/src/general-add-tariff.html +14 -8
  46. package/src/general-add-tariff.js +0 -1
  47. package/src/handle-input.js +94 -106
  48. package/src/handle-output.js +109 -0
  49. package/src/receive-price-functions.js +3 -3
  50. package/src/schedule-merger-functions.js +98 -0
  51. package/src/schedule-merger.html +135 -0
  52. package/src/schedule-merger.js +108 -0
  53. package/src/strategy-best-save.html +38 -1
  54. package/src/strategy-best-save.js +17 -63
  55. package/src/strategy-fixed-schedule.html +339 -0
  56. package/src/strategy-fixed-schedule.js +84 -0
  57. package/src/strategy-functions.js +35 -0
  58. package/src/strategy-lowest-price.html +76 -38
  59. package/src/strategy-lowest-price.js +16 -35
  60. package/src/utils.js +75 -2
  61. package/test/commands-input-best-save.test.js +142 -0
  62. package/test/commands-input-lowest-price.test.js +149 -0
  63. package/test/commands-input-schedule-merger.test.js +128 -0
  64. package/test/data/best-save-overlap-result.json +5 -1
  65. package/test/data/best-save-result.json +4 -0
  66. package/test/data/commands-result-best-save.json +383 -0
  67. package/test/data/commands-result-lowest-price.json +340 -0
  68. package/test/data/fixed-schedule-result.json +353 -0
  69. package/test/data/lowest-price-result-cont-max-fail.json +5 -1
  70. package/test/data/lowest-price-result-cont-max.json +3 -1
  71. package/test/data/lowest-price-result-cont.json +8 -1
  72. package/test/data/lowest-price-result-missing-end.json +8 -3
  73. package/test/data/lowest-price-result-neg-cont.json +27 -0
  74. package/test/data/lowest-price-result-neg-split.json +23 -0
  75. package/test/data/lowest-price-result-split-allday.json +3 -1
  76. package/test/data/lowest-price-result-split-allday10.json +1 -0
  77. package/test/data/lowest-price-result-split-max.json +3 -1
  78. package/test/data/lowest-price-result-split.json +3 -1
  79. package/test/data/merge-schedule-data.js +238 -0
  80. package/test/data/negative-prices.json +197 -0
  81. package/test/data/nordpool-event-prices.json +96 -480
  82. package/test/data/nordpool-zero-prices.json +90 -0
  83. package/test/data/reconfigResult.js +1 -0
  84. package/test/data/result.js +1 -0
  85. package/test/data/tibber-result-end-0-24h.json +12 -2
  86. package/test/data/tibber-result-end-0.json +12 -2
  87. package/test/data/tibber-result.json +1 -0
  88. package/test/receive-price.test.js +22 -0
  89. package/test/schedule-merger-functions.test.js +101 -0
  90. package/test/schedule-merger-test-utils.js +27 -0
  91. package/test/schedule-merger.test.js +130 -0
  92. package/test/send-config-input.test.js +45 -2
  93. package/test/strategy-best-save-test-utils.js +1 -1
  94. package/test/strategy-best-save.test.js +45 -0
  95. package/test/strategy-fixed-schedule.test.js +117 -0
  96. package/test/strategy-heat-capacitor.test.js +1 -1
  97. package/test/strategy-lowest-price-functions.test.js +1 -1
  98. package/test/strategy-lowest-price-test-utils.js +31 -0
  99. package/test/strategy-lowest-price.test.js +55 -45
  100. package/test/test-utils.js +43 -36
  101. package/test/utils.test.js +13 -0
  102. package/docs/images/node-power-saver.png +0 -0
  103. package/docs/nodes/power-saver.md +0 -23
  104. package/src/power-saver.html +0 -116
  105. package/src/power-saver.js +0 -260
  106. package/test/commands-input.test.js +0 -47
  107. package/test/power-saver.test.js +0 -189
@@ -1,7 +1,7 @@
1
1
  const { DateTime } = require("luxon");
2
- const { booleanConfig, makeSchedule, loadDayData } = require("./utils");
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 originalConfig = {
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
- node.context().set("config", originalConfig);
24
- node.contextStorage = originalConfig.contextStorage;
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
- handleStrategyInput(node, msg, doPlanning);
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, _, priceData, _, dateDayBefore, _) {
39
- const dataDayBefore = loadDayData(node, dateDayBefore);
40
- const values = [...dataDayBefore.hours.map((h) => h.price), ...priceData.map((pd) => pd.value)];
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
- const schedule = makeSchedule(onOff, startTimes, !onOff[0]);
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] = inputConfig[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, node.contextStorage);
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
@@ -55,6 +55,10 @@
55
55
  {
56
56
  "time": "2021-06-20T01:50:00.470+02:00",
57
57
  "value": true
58
+ },
59
+ {
60
+ "time": "2021-06-20T02:50:00.470+02:00",
61
+ "value": false
58
62
  }
59
63
  ],
60
64
  "hours": [