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
@@ -0,0 +1,90 @@
1
+ {
2
+ "_msgid": "ccc08b141f59afdb",
3
+ "payload": "0.5",
4
+ "data": {
5
+ "entity_id": "sensor.nordpool_kwh_krsand_nok_2_10_025",
6
+ "state": "0.5",
7
+ "attributes": {
8
+ "current_price": 0.5,
9
+ "average": 0.41041666666666665,
10
+ "off_peak_1": 0.37125,
11
+ "off_peak_2": 0.21000000000000002,
12
+ "peak": 0.5225,
13
+ "min": 0.1,
14
+ "max": 0.67,
15
+ "unit": "kWh",
16
+ "currency": "NOK",
17
+ "country": "Norway",
18
+ "region": "Kr.sand",
19
+ "low price": false,
20
+ "tomorrow_valid": true,
21
+ "today": [
22
+ 0.36, 0.34, 0.33, 0.31, 0.29, 0.31, 0.47, 0.56, 0.67, 0.58, 0.56, 0.53, 0.53, 0.51, 0.5, 0.48, 0.49, 0.48, 0.39,
23
+ 0.32, 0.31, 0.28, 0.15, 0.1
24
+ ],
25
+ "tomorrow": [
26
+ 0.01, 0, 0, 0, 0, 0, 0.11, 0.13, 0.14, 0.14, 0.14, 0.14, 0.13, 0.13, 0.13, 0.14, 0.16, 0.18, 0.16, 0.13, 0.11,
27
+ 0.11, 0.01, 0
28
+ ],
29
+ "raw_today": [
30
+ { "start": "2022-11-10T00:00:00+01:00", "end": "2022-11-10T01:00:00+01:00", "value": 0.36 },
31
+ { "start": "2022-11-10T01:00:00+01:00", "end": "2022-11-10T02:00:00+01:00", "value": 0.34 },
32
+ { "start": "2022-11-10T02:00:00+01:00", "end": "2022-11-10T03:00:00+01:00", "value": 0.33 },
33
+ { "start": "2022-11-10T03:00:00+01:00", "end": "2022-11-10T04:00:00+01:00", "value": 0.31 },
34
+ { "start": "2022-11-10T04:00:00+01:00", "end": "2022-11-10T05:00:00+01:00", "value": 0.29 },
35
+ { "start": "2022-11-10T05:00:00+01:00", "end": "2022-11-10T06:00:00+01:00", "value": 0.31 },
36
+ { "start": "2022-11-10T06:00:00+01:00", "end": "2022-11-10T07:00:00+01:00", "value": 0.47 },
37
+ { "start": "2022-11-10T07:00:00+01:00", "end": "2022-11-10T08:00:00+01:00", "value": 0.56 },
38
+ { "start": "2022-11-10T08:00:00+01:00", "end": "2022-11-10T09:00:00+01:00", "value": 0.67 },
39
+ { "start": "2022-11-10T09:00:00+01:00", "end": "2022-11-10T10:00:00+01:00", "value": 0.58 },
40
+ { "start": "2022-11-10T10:00:00+01:00", "end": "2022-11-10T11:00:00+01:00", "value": 0.56 },
41
+ { "start": "2022-11-10T11:00:00+01:00", "end": "2022-11-10T12:00:00+01:00", "value": 0.53 },
42
+ { "start": "2022-11-10T12:00:00+01:00", "end": "2022-11-10T13:00:00+01:00", "value": 0.53 },
43
+ { "start": "2022-11-10T13:00:00+01:00", "end": "2022-11-10T14:00:00+01:00", "value": 0.51 },
44
+ { "start": "2022-11-10T14:00:00+01:00", "end": "2022-11-10T15:00:00+01:00", "value": 0.5 },
45
+ { "start": "2022-11-10T15:00:00+01:00", "end": "2022-11-10T16:00:00+01:00", "value": 0.48 },
46
+ { "start": "2022-11-10T16:00:00+01:00", "end": "2022-11-10T17:00:00+01:00", "value": 0.49 },
47
+ { "start": "2022-11-10T17:00:00+01:00", "end": "2022-11-10T18:00:00+01:00", "value": 0.48 },
48
+ { "start": "2022-11-10T18:00:00+01:00", "end": "2022-11-10T19:00:00+01:00", "value": 0.39 },
49
+ { "start": "2022-11-10T19:00:00+01:00", "end": "2022-11-10T20:00:00+01:00", "value": 0.32 },
50
+ { "start": "2022-11-10T20:00:00+01:00", "end": "2022-11-10T21:00:00+01:00", "value": 0.31 },
51
+ { "start": "2022-11-10T21:00:00+01:00", "end": "2022-11-10T22:00:00+01:00", "value": 0.28 },
52
+ { "start": "2022-11-10T22:00:00+01:00", "end": "2022-11-10T23:00:00+01:00", "value": 0.15 },
53
+ { "start": "2022-11-10T23:00:00+01:00", "end": "2022-11-11T00:00:00+01:00", "value": 0.1 }
54
+ ],
55
+ "raw_tomorrow": [
56
+ { "start": "2022-11-11T00:00:00+01:00", "end": "2022-11-11T01:00:00+01:00", "value": 0.01 },
57
+ { "start": "2022-11-11T01:00:00+01:00", "end": "2022-11-11T02:00:00+01:00", "value": 0 },
58
+ { "start": "2022-11-11T02:00:00+01:00", "end": "2022-11-11T03:00:00+01:00", "value": 0 },
59
+ { "start": "2022-11-11T03:00:00+01:00", "end": "2022-11-11T04:00:00+01:00", "value": 0 },
60
+ { "start": "2022-11-11T04:00:00+01:00", "end": "2022-11-11T05:00:00+01:00", "value": 0 },
61
+ { "start": "2022-11-11T05:00:00+01:00", "end": "2022-11-11T06:00:00+01:00", "value": 0 },
62
+ { "start": "2022-11-11T06:00:00+01:00", "end": "2022-11-11T07:00:00+01:00", "value": 0.11 },
63
+ { "start": "2022-11-11T07:00:00+01:00", "end": "2022-11-11T08:00:00+01:00", "value": 0.13 },
64
+ { "start": "2022-11-11T08:00:00+01:00", "end": "2022-11-11T09:00:00+01:00", "value": 0.14 },
65
+ { "start": "2022-11-11T09:00:00+01:00", "end": "2022-11-11T10:00:00+01:00", "value": 0.14 },
66
+ { "start": "2022-11-11T10:00:00+01:00", "end": "2022-11-11T11:00:00+01:00", "value": 0.14 },
67
+ { "start": "2022-11-11T11:00:00+01:00", "end": "2022-11-11T12:00:00+01:00", "value": 0.14 },
68
+ { "start": "2022-11-11T12:00:00+01:00", "end": "2022-11-11T13:00:00+01:00", "value": 0.13 },
69
+ { "start": "2022-11-11T13:00:00+01:00", "end": "2022-11-11T14:00:00+01:00", "value": 0.13 },
70
+ { "start": "2022-11-11T14:00:00+01:00", "end": "2022-11-11T15:00:00+01:00", "value": 0.13 },
71
+ { "start": "2022-11-11T15:00:00+01:00", "end": "2022-11-11T16:00:00+01:00", "value": 0.14 },
72
+ { "start": "2022-11-11T16:00:00+01:00", "end": "2022-11-11T17:00:00+01:00", "value": 0.16 },
73
+ { "start": "2022-11-11T17:00:00+01:00", "end": "2022-11-11T18:00:00+01:00", "value": 0.18 },
74
+ { "start": "2022-11-11T18:00:00+01:00", "end": "2022-11-11T19:00:00+01:00", "value": 0.16 },
75
+ { "start": "2022-11-11T19:00:00+01:00", "end": "2022-11-11T20:00:00+01:00", "value": 0.13 },
76
+ { "start": "2022-11-11T20:00:00+01:00", "end": "2022-11-11T21:00:00+01:00", "value": 0.11 },
77
+ { "start": "2022-11-11T21:00:00+01:00", "end": "2022-11-11T22:00:00+01:00", "value": 0.11 },
78
+ { "start": "2022-11-11T22:00:00+01:00", "end": "2022-11-11T23:00:00+01:00", "value": 0.01 },
79
+ { "start": "2022-11-11T23:00:00+01:00", "end": "2022-11-12T00:00:00+01:00", "value": 0 }
80
+ ],
81
+ "unit_of_measurement": "NOK/kWh",
82
+ "icon": "mdi:flash",
83
+ "friendly_name": "nordpool_kwh_krsand_nok_2_10_025"
84
+ },
85
+ "context": { "id": "01GHGTS9D4YTFD3BETMHQ5QTMR", "parent_id": null, "user_id": null },
86
+ "last_changed": "2022-11-10T13:13:35.396Z",
87
+ "last_updated": "2022-11-10T13:13:35.396Z",
88
+ "timeSinceChangedMs": 1145332
89
+ }
90
+ }
@@ -12,6 +12,7 @@ module.exports = {
12
12
  { time: "2021-06-20T01:50:00.360+02:00", value: true },
13
13
  { time: "2021-06-20T01:50:00.410+02:00", value: false },
14
14
  { time: "2021-06-20T01:50:00.440+02:00", value: true },
15
+ { time: "2021-06-20T02:50:00.470+02:00", value: false },
15
16
  ],
16
17
  hours: [
17
18
  { price: 0.2494, onOff: false, start: "2021-06-20T01:50:00.000+02:00", saving: 0.0395 },
@@ -170,6 +170,7 @@ module.exports = {
170
170
  ],
171
171
  source: "Other",
172
172
  config: {
173
+ hasChanged: false,
173
174
  maxHoursToSaveInSequence: 3,
174
175
  minHoursOnAfterMaxSequenceSaved: 2,
175
176
  minSaving: 0.001,
@@ -19,6 +19,11 @@
19
19
  "time": "2021-12-15T21:00:00.000+01:00",
20
20
  "value": true,
21
21
  "countHours": 3
22
+ },
23
+ {
24
+ "time": "2021-12-16T00:00:00.000+01:00",
25
+ "value": false,
26
+ "countHours": null
22
27
  }
23
28
  ],
24
29
  "hours": [
@@ -313,6 +318,7 @@
313
318
  ],
314
319
  "source": "Tibber",
315
320
  "config": {
321
+ "hasChanged": false,
316
322
  "contextStorage": "default",
317
323
  "fromTime": "16",
318
324
  "toTime": "00",
@@ -321,9 +327,13 @@
321
327
  "doNotSplit": false,
322
328
  "sendCurrentValueWhenRescheduling": true,
323
329
  "outputIfNoSchedule": false,
324
- "outputOutsidePeriod": false
330
+ "outputValueForOff": false,
331
+ "outputValueForOfftype": "bool",
332
+ "outputValueForOn": true,
333
+ "outputValueForOntype": "bool",
334
+ "outputOutsidePeriod": false,
335
+ "override": "auto"
325
336
  },
326
- "sentOnCommand": false,
327
337
  "time": "2021-10-11T10:00:00.000+02:00",
328
338
  "version": "1.2.3"
329
339
  }
@@ -9,6 +9,11 @@
9
9
  "time": "2021-12-14T21:00:00.000+01:00",
10
10
  "value": true,
11
11
  "countHours": 3
12
+ },
13
+ {
14
+ "time": "2021-12-15T00:00:00.000+01:00",
15
+ "value": false,
16
+ "countHours": null
12
17
  }
13
18
  ],
14
19
  "hours": [
@@ -159,6 +164,7 @@
159
164
  ],
160
165
  "source": "Tibber",
161
166
  "config": {
167
+ "hasChanged": false,
162
168
  "contextStorage": "default",
163
169
  "fromTime": "16",
164
170
  "toTime": "00",
@@ -167,9 +173,13 @@
167
173
  "doNotSplit": false,
168
174
  "sendCurrentValueWhenRescheduling": true,
169
175
  "outputIfNoSchedule": false,
170
- "outputOutsidePeriod": false
176
+ "outputValueForOff": false,
177
+ "outputValueForOfftype": "bool",
178
+ "outputValueForOn": true,
179
+ "outputValueForOntype": "bool",
180
+ "outputOutsidePeriod": false,
181
+ "override": "auto"
171
182
  },
172
- "sentOnCommand": false,
173
183
  "time": "2021-10-11T10:00:00.000+02:00",
174
184
  "version": "1.2.3"
175
185
  }
@@ -363,6 +363,7 @@
363
363
  ],
364
364
  "source": "Tibber",
365
365
  "config": {
366
+ "hasChanged": false,
366
367
  "maxHoursToSaveInSequence": 4,
367
368
  "minHoursOnAfterMaxSequenceSaved": 2,
368
369
  "minSaving": 0.001,
@@ -142,4 +142,26 @@ describe("receive-price node", function () {
142
142
  n1.receive({ payload: nordpoolPrices.payload });
143
143
  });
144
144
  });
145
+ it("should convert nordpool zero prices", function (done) {
146
+ const nordpoolPrices = require("./data/nordpool-zero-prices.json");
147
+ const flow = [
148
+ {
149
+ id: "n1",
150
+ type: "ps-receive-price",
151
+ name: "Receive prices",
152
+ wires: [["n2"]],
153
+ },
154
+ { id: "n2", type: "helper" },
155
+ ];
156
+ helper.load(receivePrices, flow, function () {
157
+ const n1 = helper.getNode("n1");
158
+ const n2 = helper.getNode("n2");
159
+ n2.on("input", function (msg) {
160
+ expect(msg.payload.priceData.length).toEqual(48);
161
+ expect(msg.payload.source).toEqual("Nordpool");
162
+ done();
163
+ });
164
+ n1.receive(nordpoolPrices);
165
+ });
166
+ });
145
167
  });
@@ -0,0 +1,101 @@
1
+ const { DateTime } = require("luxon");
2
+ const expect = require("expect");
3
+ const { validateSchedule, saveSchedule, mergeSchedules, runSchedule } = require("../src/schedule-merger-functions");
4
+ const bestSaveResult = require("./data/best-save-result.json");
5
+ const mergeData = require("./data/merge-schedule-data.js");
6
+ const cloneDeep = require("lodash.clonedeep");
7
+
8
+ describe("schedule-merger-functions", () => {
9
+ it("saveSchedule", () => {
10
+ const node = useNodeMock();
11
+ const msg = { payload: cloneDeep(bestSaveResult) };
12
+ msg.payload.strategyNodeId = "1";
13
+ msg.payload.hours[0].onOff = false;
14
+ saveSchedule(node, msg);
15
+ expect(node.context().get()["1"]).toEqual(msg.payload);
16
+
17
+ msg.payload.strategyNodeId = "2";
18
+ msg.payload.hours[0].onOff = true;
19
+ saveSchedule(node, msg);
20
+ expect(node.context().get()["1"]).toEqual(msg.payload);
21
+ expect(node.context().get()["2"]).toEqual(msg.payload);
22
+ expect(node.context().get()["1"].hours.onOff).toBeFalsy;
23
+ expect(node.context().get()["2"].hours.onOff).toBeTruthy;
24
+ });
25
+
26
+ it("mergeSchedule", () => {
27
+ const messages = {};
28
+ Object.keys(mergeData).forEach((ds) => {
29
+ messages[ds] = {
30
+ payload: {
31
+ strategyNodeId: ds,
32
+ hours: mergeData[ds],
33
+ },
34
+ };
35
+ });
36
+
37
+ let node = useNodeMock();
38
+ saveSchedule(node, messages.allOff);
39
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([false, false, false, false, false]);
40
+ saveSchedule(node, messages.allOn);
41
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([true, true, true, true, true]);
42
+ expect(mergeSchedules(node, "AND").map((h) => h.onOff)).toEqual([false, false, false, false, false]);
43
+
44
+ node = useNodeMock();
45
+ saveSchedule(node, messages.someOn);
46
+ saveSchedule(node, messages.allOn);
47
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([true, true, true, true, true]);
48
+ expect(mergeSchedules(node, "AND").map((h) => h.onOff)).toEqual([true, false, true, false, true]);
49
+
50
+ node = useNodeMock();
51
+ saveSchedule(node, messages.someOn);
52
+ saveSchedule(node, messages.allOff);
53
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([true, false, true, false, true]);
54
+ expect(mergeSchedules(node, "AND").map((h) => h.onOff)).toEqual([false, false, false, false, false]);
55
+ saveSchedule(node, messages.hourLater);
56
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([true, false, false, true, true]);
57
+ expect(mergeSchedules(node, "AND").map((h) => h.onOff)).toEqual([true, false, false, true, true]);
58
+
59
+ saveSchedule(node, messages.someOn);
60
+ saveSchedule(node, messages.allOff);
61
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([true, false, true, false, true]);
62
+ expect(mergeSchedules(node, "AND").map((h) => h.onOff)).toEqual([false, false, false, false, false]);
63
+
64
+ node = useNodeMock();
65
+ saveSchedule(node, messages.someOn);
66
+ saveSchedule(node, messages.lessHours);
67
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([false, true, false]);
68
+
69
+ node = useNodeMock();
70
+ saveSchedule(node, messages.someOn);
71
+ saveSchedule(node, messages.moreHours);
72
+ expect(mergeSchedules(node, "OR").map((h) => h.onOff)).toEqual([
73
+ true,
74
+ true,
75
+ false,
76
+ false,
77
+ false,
78
+ true,
79
+ true,
80
+ false,
81
+ ]);
82
+ });
83
+ });
84
+
85
+ // Node mock
86
+ const useNodeMock = function () {
87
+ let savedSchedules = {};
88
+ const set = function (_, obj) {
89
+ savedSchedules = { ...obj };
90
+ };
91
+ const get = function () {
92
+ return savedSchedules;
93
+ };
94
+ const context = function () {
95
+ return { get, set };
96
+ };
97
+ const warn = function (msg) {
98
+ console.log(msg);
99
+ };
100
+ return { context, warn };
101
+ };
@@ -0,0 +1,27 @@
1
+ function makeFlow(logicFunction, outputIfNoSchedule = true) {
2
+ return [
3
+ {
4
+ id: "n1",
5
+ type: "ps-schedule-merger",
6
+ name: "test name",
7
+ logicFunction,
8
+ outputIfNoSchedule,
9
+ schedulingDelay: 10, // May need to increase on a slow computer
10
+ sendCurrentValueWhenRescheduling: true,
11
+ wires: [["n3"], ["n4"], ["n2"]],
12
+ },
13
+ { id: "n2", type: "helper" },
14
+ { id: "n3", type: "helper" },
15
+ { id: "n4", type: "helper" },
16
+ ];
17
+ }
18
+
19
+ function makePayload(strategyNodeId, hours) {
20
+ const payload = {
21
+ strategyNodeId,
22
+ hours,
23
+ };
24
+ return payload;
25
+ }
26
+
27
+ module.exports = { makeFlow, makePayload };
@@ -0,0 +1,130 @@
1
+ const expect = require("expect");
2
+ const helper = require("node-red-node-test-helper");
3
+ const scheduleMerger = require("../src/schedule-merger.js");
4
+ const { equalHours } = require("./test-utils");
5
+ const { allOff, allOn, someOn, theOtherOn } = require("./data/merge-schedule-data.js");
6
+ const { makeFlow, makePayload } = require("./schedule-merger-test-utils.js");
7
+
8
+ helper.init(require.resolve("node-red"));
9
+
10
+ describe("schedule-merger node", function () {
11
+ beforeEach(function (done) {
12
+ helper.startServer(done);
13
+ });
14
+
15
+ afterEach(function (done) {
16
+ helper.unload().then(function () {
17
+ helper.stopServer(done);
18
+ });
19
+ });
20
+
21
+ it("should be loaded", function (done) {
22
+ const flow = [{ id: "n1", type: "ps-schedule-merger", name: "test name" }];
23
+ helper.load(scheduleMerger, flow, function () {
24
+ const n1 = helper.getNode("n1");
25
+ expect(n1).toHaveProperty("name", "test name");
26
+ done();
27
+ });
28
+ });
29
+
30
+ it("can merge two schedules with OR", function (done) {
31
+ const flow = makeFlow("OR");
32
+ helper.load(scheduleMerger, flow, function () {
33
+ const n1 = helper.getNode("n1");
34
+ const n2 = helper.getNode("n2");
35
+ n2.on("input", function (msg) {
36
+ expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
37
+ n1.warn.should.not.be.called;
38
+ done();
39
+ });
40
+ n1.receive({ payload: makePayload("s1", someOn) });
41
+ n1.receive({ payload: makePayload("s2", allOff) });
42
+ });
43
+ });
44
+
45
+ it("can merge two schedules with AND", function (done) {
46
+ const flow = makeFlow("AND", false);
47
+ helper.load(scheduleMerger, flow, function () {
48
+ const n1 = helper.getNode("n1");
49
+ const n2 = helper.getNode("n2");
50
+ n2.on("input", function (msg) {
51
+ expect(equalHours(someOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
52
+ expect(msg.payload.schedule.length).toBe(6);
53
+ expect(msg.payload.schedule[5].value).toBeFalsy();
54
+ expect(msg.payload.schedule[5].countHours).toBeNull();
55
+ n1.warn.should.not.be.called;
56
+ done();
57
+ });
58
+ n1.receive({ payload: makePayload("s1", someOn) });
59
+ n1.receive({ payload: makePayload("s2", allOn) });
60
+ });
61
+ });
62
+
63
+ it("can merge two schedules with OR all on", function (done) {
64
+ const flow = makeFlow("OR");
65
+ helper.load(scheduleMerger, flow, function () {
66
+ const n1 = helper.getNode("n1");
67
+ const n2 = helper.getNode("n2");
68
+ n2.on("input", function (msg) {
69
+ expect(equalHours(allOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
70
+ expect(msg.payload.schedule.length).toBe(1);
71
+ n1.warn.should.not.be.called;
72
+ done();
73
+ });
74
+ n1.receive({ payload: makePayload("s1", someOn) });
75
+ n1.receive({ payload: makePayload("s2", allOn) });
76
+ });
77
+ });
78
+
79
+ it("can merge two schedules with AND all off", function (done) {
80
+ const flow = makeFlow("AND");
81
+ helper.load(scheduleMerger, flow, function () {
82
+ const n1 = helper.getNode("n1");
83
+ const n2 = helper.getNode("n2");
84
+ n2.on("input", function (msg) {
85
+ expect(equalHours(allOff, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
86
+ expect(msg.payload.schedule.length).toBe(2);
87
+ expect(msg.payload.schedule[1].value).toBeTruthy();
88
+ expect(msg.payload.schedule[1].countHours).toBeNull();
89
+ n1.warn.should.not.be.called;
90
+ done();
91
+ });
92
+ n1.receive({ payload: makePayload("s1", someOn) });
93
+ n1.receive({ payload: makePayload("s2", allOff) });
94
+ });
95
+ });
96
+
97
+ it("can merge three schedules with OR", function (done) {
98
+ const flow = makeFlow("OR");
99
+ helper.load(scheduleMerger, flow, function () {
100
+ const n1 = helper.getNode("n1");
101
+ const n2 = helper.getNode("n2");
102
+ n2.on("input", function (msg) {
103
+ expect(equalHours(allOn, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
104
+ expect(msg.payload.schedule.length).toBe(1);
105
+ n1.warn.should.not.be.called;
106
+ done();
107
+ });
108
+ n1.receive({ payload: makePayload("s1", someOn) });
109
+ n1.receive({ payload: makePayload("s2", allOff) });
110
+ n1.receive({ payload: makePayload("s3", theOtherOn) });
111
+ });
112
+ });
113
+
114
+ it("can merge three schedules with AND", function (done) {
115
+ const flow = makeFlow("AND", false);
116
+ helper.load(scheduleMerger, flow, function () {
117
+ const n1 = helper.getNode("n1");
118
+ const n2 = helper.getNode("n2");
119
+ n2.on("input", function (msg) {
120
+ expect(equalHours(allOff, msg.payload.hours, ["price", "onOff", "start"])).toBeTruthy();
121
+ expect(msg.payload.schedule.length).toBe(1);
122
+ n1.warn.should.not.be.called;
123
+ done();
124
+ });
125
+ n1.receive({ payload: makePayload("s1", someOn) });
126
+ n1.receive({ payload: makePayload("s2", allOn) });
127
+ n1.receive({ payload: makePayload("s3", theOtherOn) });
128
+ });
129
+ });
130
+ });
@@ -37,12 +37,12 @@ describe("send config as input", () => {
37
37
  break;
38
38
  case 2:
39
39
  pass++;
40
- expect(msg.payload.schedule.length).toEqual(1);
40
+ expect(msg.payload.schedule.length).toEqual(2);
41
41
  n1.receive({ payload: makePayload(prices, testPlan.time) });
42
42
  break;
43
43
  case 3:
44
44
  pass++;
45
- expect(msg.payload.schedule.length).toEqual(1);
45
+ expect(msg.payload.schedule.length).toEqual(2);
46
46
  done();
47
47
  }
48
48
  });
@@ -113,6 +113,49 @@ describe("send config as input", () => {
113
113
  n1.receive({ payload: makePayload(prices, testPlan.time) });
114
114
  });
115
115
  });
116
+ it("can override", function (done) {
117
+ const flow = makeFlow(3, 2, false);
118
+ helper.load(bestSave, flow, function () {
119
+ const n1 = helper.getNode("n1");
120
+ const n2 = helper.getNode("n2");
121
+ const n3 = helper.getNode("n3");
122
+ const n4 = helper.getNode("n4");
123
+ let countOn = 0;
124
+ let countOff = 0;
125
+ let pass = 0;
126
+ n2.on("input", function (msg) {
127
+ pass++;
128
+ n1.warn.should.not.be.called;
129
+ if (pass === 1) {
130
+ setTimeout(() => {
131
+ console.log("countOn = " + countOn + ", countOff = " + countOff);
132
+ expect(countOn).toEqual(2);
133
+ expect(countOff).toEqual(2);
134
+ done();
135
+ }, 900);
136
+ }
137
+ });
138
+ n3.on("input", function (msg) {
139
+ countOn++;
140
+ expect(msg).toHaveProperty("payload", true);
141
+ if (countOn === 2) {
142
+ n1.receive({ payload: { config: { override: "on" } } });
143
+ }
144
+ });
145
+ n4.on("input", function (msg) {
146
+ countOff++;
147
+ expect(msg).toHaveProperty("payload", false);
148
+ if (countOff === 1) {
149
+ n1.receive({ payload: { config: { override: "on" }, name: "wrong name" } });
150
+ }
151
+ if (countOff === 2) {
152
+ n1.receive({ payload: { config: { override: "on" } } });
153
+ }
154
+ });
155
+ const time = prices.priceData[0].start;
156
+ n1.receive({ payload: makePayload(prices, time) });
157
+ });
158
+ });
116
159
  });
117
160
 
118
161
  function makePayloadWithConfigAndPrices(prices, time) {
@@ -1,7 +1,7 @@
1
1
  const cloneDeep = require("lodash.clonedeep");
2
2
  const { DateTime } = require("luxon");
3
3
 
4
- function makeFlow(maxHoursToSaveInSequence, minHoursOnAfterMaxSequenceSaved, sendCurrentValueWhenRescheduling = false) {
4
+ function makeFlow(maxHoursToSaveInSequence, minHoursOnAfterMaxSequenceSaved, sendCurrentValueWhenRescheduling = true) {
5
5
  return [
6
6
  {
7
7
  id: "n1",
@@ -5,6 +5,7 @@ const helper = require("node-red-node-test-helper");
5
5
  const bestSave = require("../src/strategy-best-save.js");
6
6
  const prices = require("./data/converted-prices.json");
7
7
  const result = require("./data/best-save-result.json");
8
+ const convertedPrices = require("./data/converted-prices.json");
8
9
  const { testPlan: plan, equalPlan } = require("./test-utils");
9
10
  const { makeFlow } = require("./strategy-best-save-test-utils");
10
11
  const { version } = require("../package.json");
@@ -95,6 +96,50 @@ describe("ps-strategy-best-save node", function () {
95
96
  n1.receive({ payload: makePayload(prices, plan.time) });
96
97
  });
97
98
  });
99
+ it("should not send output when rescheduling", function (done) {
100
+ const flow = makeFlow(3, 2, false);
101
+ helper.load(bestSave, flow, function () {
102
+ const n1 = helper.getNode("n1");
103
+ const n2 = helper.getNode("n2");
104
+ const n3 = helper.getNode("n3");
105
+ const n4 = helper.getNode("n4");
106
+ let countOn = 0;
107
+ let countOff = 0;
108
+ let pass = 0;
109
+ n2.on("input", function (msg) {
110
+ pass++;
111
+ switch (pass) {
112
+ case 1:
113
+ const payload = {
114
+ ...convertedPrices,
115
+ time: "2021-10-11T01:11:00.000+02:00",
116
+ };
117
+ n1.receive({ payload });
118
+ break;
119
+ case 2:
120
+ setTimeout(() => {
121
+ console.log("countOn = " + countOn + ", countOff = " + countOff);
122
+ expect(countOn).toEqual(0);
123
+ expect(countOff).toEqual(1);
124
+ done();
125
+ }, 100);
126
+ }
127
+ });
128
+ n3.on("input", function (msg) {
129
+ countOn++;
130
+ expect(msg).toHaveProperty("payload", true);
131
+ });
132
+ n4.on("input", function (msg) {
133
+ countOff++;
134
+ expect(msg).toHaveProperty("payload", false);
135
+ });
136
+ const payload = {
137
+ ...convertedPrices,
138
+ time: "2021-10-11T01:10:00.000+02:00",
139
+ };
140
+ n1.receive({ payload });
141
+ });
142
+ });
98
143
  });
99
144
 
100
145
  function makePayload(prices, time) {