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
|
@@ -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 },
|
package/test/data/result.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|
|
@@ -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(
|
|
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(
|
|
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 =
|
|
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) {
|