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,116 +0,0 @@
1
- <script type="text/javascript">
2
- RED.nodes.registerType("power-saver", {
3
- category: "Power Saver",
4
- color: "#a6bbcf",
5
- defaults: {
6
- name: { value: "Power Saver" },
7
- maxHoursToSaveInSequence: {
8
- value: 3,
9
- required: true,
10
- validate: RED.validators.number(),
11
- },
12
- minHoursOnAfterMaxSequenceSaved: {
13
- value: 2,
14
- required: true,
15
- validate: RED.validators.number(),
16
- },
17
- minSaving: {
18
- value: 0.01,
19
- required: true,
20
- validate: RED.validators.number(),
21
- },
22
- sendCurrentValueWhenRescheduling: {
23
- value: true,
24
- required: true,
25
- // validate: RED.validators.number(),
26
- align: "left",
27
- },
28
- outputIfNoSchedule: { value: "true", required: true, align: "left" },
29
- scheduleOnlyFromCurrentTime: {
30
- value: "true",
31
- required: true,
32
- align: "left",
33
- },
34
- },
35
- inputs: 1,
36
- outputs: 3,
37
- icon: "font-awesome/fa-eur",
38
- color: "#FFCC66",
39
- label: function () {
40
- return this.name || "Power Saver";
41
- },
42
- outputLabels: ["on", "off", "schedule"],
43
- oneditprepare: function () {
44
- $("#node-input-outputIfNoSchedule").typedInput({
45
- types: [
46
- {
47
- value: "onoff",
48
- options: [
49
- { value: "true", label: "On" },
50
- { value: "false", label: "Off" },
51
- ],
52
- },
53
- ],
54
- });
55
- $("#node-input-scheduleOnlyFromCurrentTime").typedInput({
56
- types: [
57
- {
58
- value: "nowOrStart",
59
- options: [
60
- { value: "false", label: "Whole data set" },
61
- { value: "true", label: "From current hour" },
62
- ],
63
- },
64
- ],
65
- });
66
- },
67
- });
68
- </script>
69
-
70
- <script type="text/html" data-template-name="power-saver">
71
- <div class="form-row">
72
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
73
- <input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
74
- </div>
75
- <h3>Rules</h3>
76
- <div class="form-row">
77
- <label for="node-input-maxHoursToSaveInSequence"><i class="fa fa-arrows-h"></i> Max per sequence</label>
78
- <input type="text" id="node-input-maxHoursToSaveInSequence" style="width: 80px" placeholder="Max hours to save in sequence">
79
- </div>
80
- <div class="form-row">
81
- <label for="node-input-minHoursOnAfterMaxSequenceSaved"><i class="fa fa-ellipsis-h"></i> Min recover</label>
82
- <input type="text"
83
- id="node-input-minHoursOnAfterMaxSequenceSaved"
84
- style="width: 80px"
85
- placeholder="Min hours on after a max sequence">
86
- </div>
87
- <div class="form-row">
88
- <label for="node-input-minSaving"><i class="fa fa-eur"></i> Min saving</label>
89
- <input type="text" id="node-input-minSaving" placeholder="Minimum to save for turning off" style="width: 80px">
90
- </div>
91
- <div class="form-row">
92
- <label for="node-input-scheduleOnlyFromCurrentTime">Schedule for</label>
93
- <input type="text" id="node-input-scheduleOnlyFromCurrentTime" style="width: 160px">
94
- </label>
95
- </div>
96
- <h3>Output</h3>
97
- <div class="form-row">
98
- <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
99
- <input type="checkbox"
100
- id="node-input-sendCurrentValueWhenRescheduling"
101
- style="display:inline-block; width:22px; vertical-align:top;"
102
- autocomplete="off"><span>Send when rescheduling</span>
103
- </label>
104
- </div>
105
- <div class="form-row">
106
- <label for="node-input-outputIfNoSchedule">If no schedule, send</label>
107
- <input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
108
- </label>
109
- </div>
110
- </script>
111
-
112
- <script type="text/markdown" data-help-name="power-saver">
113
- A node you can use to save money by turning off and on a switch based on power prices.
114
-
115
- Please read more in the [node documentation](https://powersaver.no/nodes/power-saver)
116
- </script>
@@ -1,260 +0,0 @@
1
- const { DateTime } = require("luxon");
2
- const {
3
- countAtEnd,
4
- extractPlanForDate,
5
- getDiff,
6
- getEffectiveConfig,
7
- getSavings,
8
- getStartAtIndex,
9
- loadDayData,
10
- makeSchedule,
11
- validationFailure,
12
- } = require("./utils");
13
- const { convertMsg } = require("./receive-price-functions");
14
- const { calculate } = require("./strategy-best-save-functions");
15
-
16
- let schedulingTimeout = null;
17
-
18
- module.exports = function (RED) {
19
- function PowerSaverNode(config) {
20
- RED.nodes.createNode(this, config);
21
- const node = this;
22
-
23
- const originalConfig = {
24
- maxHoursToSaveInSequence: config.maxHoursToSaveInSequence,
25
- minHoursOnAfterMaxSequenceSaved: config.minHoursOnAfterMaxSequenceSaved,
26
- minSaving: parseFloat(config.minSaving),
27
- sendCurrentValueWhenRescheduling: config.sendCurrentValueWhenRescheduling,
28
- outputIfNoSchedule: config.outputIfNoSchedule === "true",
29
- scheduleOnlyFromCurrentTime: config.scheduleOnlyFromCurrentTime === "true",
30
- };
31
- node.context().set("config", originalConfig);
32
-
33
- node.on("close", function () {
34
- clearTimeout(schedulingTimeout);
35
- });
36
-
37
- node.on("input", function (msg) {
38
- const effectiveConfig = getEffectiveConfig(node, msg);
39
- const priceData = getPriceData(node, msg);
40
- if (!priceData) {
41
- return;
42
- }
43
- const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
44
-
45
- // Store config variables in node
46
- Object.keys(effectiveConfig).forEach((key) => (node[key] = effectiveConfig[key]));
47
-
48
- clearTimeout(schedulingTimeout);
49
-
50
- const dates = [...new Set(priceData.map((v) => DateTime.fromISO(v.start).toISODate()))];
51
-
52
- // Load data from day before
53
- const dateToday = DateTime.fromISO(dates[0]);
54
- const dateDayBefore = DateTime.fromISO(dates[0]).plus({ days: -1 });
55
-
56
- // Make plan
57
- const startAtIndex = getStartAtIndex(effectiveConfig, priceData, planFromTime);
58
- const dataJustBefore = loadDataJustBefore(node, dateDayBefore, dateToday, startAtIndex);
59
- const values = priceData.map((d) => d.value).slice(startAtIndex);
60
- const startTimes = priceData.map((d) => d.start).slice(startAtIndex);
61
- const onOffBefore = dataJustBefore.hours.map((h) => h.onOff);
62
- const lastPlanHours = node.context().get("lastPlan")?.hours ?? [];
63
- const plan = makePlan(node, values, startTimes, onOffBefore);
64
- const includeFromLastPlanHours = lastPlanHours.filter(
65
- (h) => h.start < plan.hours[0].start && h.start >= priceData[0].start
66
- );
67
- adjustSavingsPassedHours(plan, includeFromLastPlanHours);
68
- plan.hours.splice(0, 0, ...includeFromLastPlanHours);
69
-
70
- // Save schedule
71
- node.context().set("lastPlan", plan);
72
- dates.forEach((d) => saveDayData(node, d, extractPlanForDate(plan, d)));
73
-
74
- // Prepare output
75
- let output1 = null;
76
- let output2 = null;
77
- let output3 = {
78
- payload: {
79
- schedule: plan.schedule,
80
- hours: plan.hours,
81
- source: priceData.source,
82
- config: effectiveConfig,
83
- },
84
- };
85
-
86
- // Find current output, and set output (if configured to do)
87
- const pastSchedule = plan.schedule.filter((entry) => DateTime.fromISO(entry.time) <= planFromTime);
88
-
89
- if (node.sendCurrentValueWhenRescheduling && pastSchedule.length > 0) {
90
- const currentValue = pastSchedule[pastSchedule.length - 1].value;
91
- output1 = currentValue ? { payload: true } : null;
92
- output2 = currentValue ? null : { payload: false };
93
- }
94
-
95
- // Delete old data
96
- deleteSavedScheduleBefore(node, dateDayBefore);
97
-
98
- // Send output
99
- node.send([output1, output2, output3]);
100
-
101
- // Run schedule
102
- schedulingTimeout = runSchedule(node, plan.schedule, planFromTime);
103
- });
104
- }
105
-
106
- RED.nodes.registerType("power-saver", PowerSaverNode);
107
- };
108
-
109
- function adjustSavingsPassedHours(plan, includeFromLastPlanHours) {
110
- const firstOnIndex = plan.hours.findIndex((h) => h.onOff);
111
- if (firstOnIndex < 0) {
112
- return;
113
- }
114
- const nextOnValue = plan.hours[firstOnIndex].price;
115
- let adjustIndex = includeFromLastPlanHours.length - 1;
116
- while (adjustIndex >= 0 && !includeFromLastPlanHours[adjustIndex].onOff) {
117
- includeFromLastPlanHours[adjustIndex].saving = getDiff(includeFromLastPlanHours[adjustIndex].price, nextOnValue);
118
- adjustIndex--;
119
- }
120
- }
121
-
122
- function getPriceData(node, msg) {
123
- const isConfigMsg = !!msg?.payload?.config;
124
- if (isConfigMsg) {
125
- return node.context().get("lastPriceData");
126
- }
127
-
128
- if (!validateMsg(node, msg)) {
129
- return null;
130
- }
131
- const input = convertMsg(msg);
132
- if (!validateInput(node, input)) {
133
- return null;
134
- }
135
-
136
- priceData = [...input.today, ...input.tomorrow];
137
- priceData.source = input.source;
138
- node.context().set("lastPriceData", priceData);
139
- return priceData;
140
- }
141
-
142
- function loadDataJustBefore(node, dateDayBefore, dateToday, startAtIndex) {
143
- const dataDayBefore = loadDayData(node, dateDayBefore);
144
- const dataToday = loadDayData(node, dateToday);
145
- return {
146
- schedule: [...dataDayBefore.schedule, ...dataToday.schedule.slice(0, startAtIndex)],
147
- hours: [...dataDayBefore.hours, ...dataToday.hours.slice(0, startAtIndex)],
148
- };
149
- }
150
-
151
- function saveDayData(node, date, plan) {
152
- node.context().set(date, plan);
153
- }
154
-
155
- function deleteSavedScheduleBefore(node, day) {
156
- let date = day;
157
- do {
158
- date = date.plus({ days: -1 });
159
- data = node.context().set(date.toISO(), undefined);
160
- } while (data);
161
- }
162
-
163
- function makePlan(node, values, startTimes, onOffBefore, firstValueNextDay) {
164
- const strategy = "mostSaved"; // TODO: Get from node settings
165
- const lastValueDayBefore = onOffBefore[onOffBefore.length - 1];
166
- const lastCountDayBefore = countAtEnd(onOffBefore, lastValueDayBefore);
167
- const onOff =
168
- strategy === "mostSaved"
169
- ? calculate(
170
- values,
171
- node.maxHoursToSaveInSequence,
172
- node.minHoursOnAfterMaxSequenceSaved,
173
- node.minSaving,
174
- lastValueDayBefore,
175
- lastCountDayBefore
176
- )
177
- : [];
178
-
179
- const schedule = makeSchedule(onOff, startTimes, lastValueDayBefore);
180
- const savings = getSavings(values, onOff, firstValueNextDay);
181
- const hours = values.map((v, i) => ({
182
- price: v,
183
- onOff: onOff[i],
184
- start: startTimes[i],
185
- saving: savings[i],
186
- }));
187
- return {
188
- hours,
189
- schedule,
190
- };
191
- }
192
-
193
- function validateMsg(node, msg) {
194
- if (!msg.payload && !msg.data?.new_state?.attributes) {
195
- validationFailure(node, "Payload missing");
196
- return false;
197
- }
198
- const payload = msg.data?.new_state?.attributes ?? msg.data?.attributes ?? msg.payload;
199
- if (typeof payload !== "object") {
200
- validationFailure(node, "Payload must be an object");
201
- return false;
202
- }
203
- return true;
204
- }
205
-
206
- function validateInput(node, input) {
207
- ["today", "tomorrow"].forEach((arr) => {
208
- if (
209
- input[arr].some((day) => {
210
- return day.start === undefined || day.value === undefined;
211
- })
212
- ) {
213
- validationFailure(node, `Malformed entries in payload.${arr}. All entries must contain start and value.`);
214
- }
215
- });
216
- if (!input.today.length && !input.tomorrow.length) {
217
- validationFailure(node, "Payload has no data");
218
- return false;
219
- }
220
-
221
- return true;
222
- }
223
-
224
- /**
225
- * Start a timer until the next time in the schedule.
226
- * When the time is out, send the relaed value and start timer for next time.
227
- * @param {*} node
228
- * @param {*} schedule
229
- * @param {*} time
230
- */
231
- function runSchedule(node, schedule, time) {
232
- let currentTime = time;
233
- let remainingSchedule = schedule.filter((entry) => DateTime.fromISO(entry.time) > time);
234
- if (remainingSchedule.length > 0) {
235
- const entry = remainingSchedule[0];
236
- const nextTime = DateTime.fromISO(entry.time);
237
- const wait = nextTime - currentTime;
238
- const onOff = entry.value ? "on" : "off";
239
- node.log("Switching " + onOff + " in " + wait + " milliseconds");
240
- const statusMessage = `Scheduled ${remainingSchedule.length} changes. Next: ${
241
- remainingSchedule[0].value ? "on" : "off"
242
- }`;
243
- node.status({ fill: "green", shape: "dot", text: statusMessage });
244
- return setTimeout(() => {
245
- sendSwitch(node, entry.value);
246
- schedulingTimeout = runSchedule(node, remainingSchedule, nextTime);
247
- }, wait);
248
- } else {
249
- const message = "No schedule";
250
- node.warn(message);
251
- node.status({ fill: "red", shape: "dot", text: message });
252
- sendSwitch(node, node.outputIfNoSchedule);
253
- }
254
- }
255
-
256
- function sendSwitch(node, onOff) {
257
- const output1 = onOff ? { payload: true } : null;
258
- const output2 = onOff ? null : { payload: false };
259
- node.send([output1, output2, null]);
260
- }
@@ -1,47 +0,0 @@
1
- const expect = require("expect");
2
- const helper = require("node-red-node-test-helper");
3
- const bestSave = require("../src/strategy-best-save.js");
4
- const prices = require("./data/converted-prices.json");
5
- const result = require("./data/best-save-result.json");
6
- const { testPlan, equalPlan } = require("./test-utils");
7
- const { makeFlow, makePayload } = require("./strategy-best-save-test-utils");
8
-
9
- helper.init(require.resolve("node-red"));
10
-
11
- describe("send command as input", () => {
12
- beforeEach(function (done) {
13
- helper.startServer(done);
14
- });
15
-
16
- afterEach(function (done) {
17
- helper.unload().then(function () {
18
- helper.stopServer(done);
19
- });
20
- });
21
-
22
- it("should send output on command", function (done) {
23
- const flow = makeFlow(3, 2, true);
24
- let pass = 1;
25
- helper.load(bestSave, flow, function () {
26
- const n1 = helper.getNode("n1");
27
- const n2 = helper.getNode("n2");
28
- n1.sendCurrentValueWhenRescheduling = true;
29
- n2.on("input", function (msg) {
30
- switch (pass) {
31
- case 1:
32
- pass++;
33
- expect(equalPlan(result, msg.payload)).toBeTruthy();
34
- expect(msg.payload.sentOnCommand).toBeFalsy();
35
- n1.receive({ payload: { commands: { sendSchedule: true } } });
36
- break;
37
- case 2:
38
- expect(equalPlan(result, msg.payload)).toBeTruthy();
39
- expect(msg.payload.sentOnCommand).toBeTruthy();
40
- done();
41
- break;
42
- }
43
- });
44
- n1.receive({ payload: makePayload(prices, testPlan.time) });
45
- });
46
- });
47
- });
@@ -1,189 +0,0 @@
1
- const cloneDeep = require("lodash.clonedeep");
2
- const expect = require("expect");
3
- const helper = require("node-red-node-test-helper");
4
- const powerSaver = require("../src/power-saver.js");
5
-
6
- const prices = require("./data/prices");
7
- const result = require("./data/result");
8
- const { testPlan: plan, makePayload } = require("./test-utils");
9
-
10
- helper.init(require.resolve("node-red"));
11
-
12
- describe("power-saver Node", function () {
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 be loaded", function (done) {
24
- const flow = [{ id: "n1", type: "power-saver", name: "test name" }];
25
- helper.load(powerSaver, flow, function () {
26
- const n1 = helper.getNode("n1");
27
- expect(n1).toHaveProperty("name", "test name");
28
- done();
29
- });
30
- });
31
-
32
- it("should log error when illegal data is received", function (done) {
33
- const flow = [{ id: "n1", type: "power-saver", name: "test name" }];
34
- helper.load(powerSaver, flow, function () {
35
- const n1 = helper.getNode("n1");
36
- n1.receive({});
37
- n1.warn.should.be.calledWithExactly("Payload missing");
38
- n1.receive({ payload: "Error" });
39
- n1.warn.should.be.calledWithExactly("Payload must be an object");
40
- n1.receive({ payload: { today: [], tomorrow: [] } });
41
- n1.warn.should.be.calledWithExactly("Payload has no data");
42
-
43
- ["start", "value"].forEach((attr) => {
44
- const testData1 = {
45
- today: cloneDeep(prices.today),
46
- tomorrow: cloneDeep(prices.tomorrow),
47
- };
48
- delete testData1.today[3][attr];
49
- n1.receive({ payload: testData1 });
50
- n1.warn.should.be.calledWithExactly(
51
- "Malformed entries in payload.today. All entries must contain start and value."
52
- );
53
- });
54
-
55
- n1.receive({ payload: cloneDeep(prices) });
56
- n1.warn.should.not.be.called;
57
- done();
58
- });
59
- });
60
- it("should send new schedule on output 3", function (done) {
61
- const flow = makeFlow(3, 2);
62
- helper.load(powerSaver, flow, function () {
63
- const n1 = helper.getNode("n1");
64
- const n2 = helper.getNode("n2");
65
- const n3 = helper.getNode("n3");
66
- const n4 = helper.getNode("n4");
67
- n2.on("input", function (msg) {
68
- expect(msg).toHaveProperty("payload", result);
69
- n1.warn.should.not.be.called;
70
- done();
71
- });
72
- n3.on("input", function (msg) {
73
- expect(msg).toHaveProperty("payload", true);
74
- });
75
- n4.on("input", function (msg) {
76
- expect(msg).toHaveProperty("payload", false);
77
- });
78
- n1.receive({ payload: makePayload(prices, plan.time) });
79
- });
80
- });
81
-
82
- it("can schedule one day", function (done) {
83
- const values = [1, 1, 1, 1, 1, 1, 1, 5, 4, 3];
84
- const schedule = [
85
- {
86
- time: "2021-06-20T01:50:00.000+02:00",
87
- value: true,
88
- countHours: 7,
89
- },
90
- {
91
- time: "2021-06-20T01:50:00.070+02:00",
92
- value: false,
93
- countHours: 2,
94
- },
95
- {
96
- time: "2021-06-20T01:50:00.090+02:00",
97
- value: true,
98
- countHours: 1,
99
- },
100
- ];
101
- const flow = makeFlow(4, 2);
102
- helper.load(powerSaver, flow, function () {
103
- const n1 = helper.getNode("n1");
104
- const n2 = helper.getNode("n2");
105
- n2.on("input", function (msg) {
106
- expect(msg.payload).toHaveProperty("schedule", schedule);
107
- n1.warn.should.not.be.called;
108
- done();
109
- });
110
- const mPrices = cloneDeep(prices);
111
- delete mPrices.tomorrow;
112
- for (i = 0; i < mPrices.today.length; i++) {
113
- mPrices.today[i].value = values[i];
114
- }
115
- const payload = makePayload(mPrices, plan.time);
116
- n1.receive({ payload });
117
- });
118
- });
119
-
120
- it("should schedule over midnight", function (done) {
121
- const values1 = [1, 1, 1, 1, 1, 1, 1, 5, 4, 3];
122
- const values2 = [2, 1, 5, 5, 5, 5, 5, 5, 5, 5];
123
- const schedule = [
124
- {
125
- time: "2021-06-20T01:50:00.000+02:00",
126
- value: true,
127
- countHours: 7,
128
- },
129
- {
130
- time: "2021-06-20T01:50:00.070+02:00",
131
- value: false,
132
- countHours: 4,
133
- },
134
- {
135
- time: "2021-06-20T01:50:00.110+02:00",
136
- value: true,
137
- countHours: 9,
138
- },
139
- ];
140
- const flow = makeFlow(4, 2);
141
- helper.load(powerSaver, flow, function () {
142
- const n1 = helper.getNode("n1");
143
- const n2 = helper.getNode("n2");
144
- n2.on("input", function (msg) {
145
- expect(msg.payload).toHaveProperty("schedule", schedule);
146
- n1.warn.should.not.be.called;
147
- done();
148
- });
149
- const mPrices = cloneDeep(prices);
150
- for (i = 0; i < mPrices.today.length; i++) {
151
- mPrices.today[i].value = values1[i];
152
- mPrices.tomorrow[i].value = values2[i];
153
- }
154
- const payload = makePayload(mPrices, plan.time);
155
- n1.receive({ payload });
156
- });
157
- });
158
- it("works for Tibber data", function (done) {
159
- const tibberData = require("./data/tibber-prices.json");
160
- const tibberResult = require("./data/tibber-result.json");
161
- const flow = makeFlow(4, 2);
162
- helper.load(powerSaver, flow, function () {
163
- const n1 = helper.getNode("n1");
164
- const n2 = helper.getNode("n2");
165
- n2.on("input", function (msg) {
166
- expect(msg).toHaveProperty("payload", tibberResult);
167
- done();
168
- });
169
- n1.receive({ ...tibberData });
170
- });
171
- });
172
- });
173
-
174
- function makeFlow(maxHoursToSaveInSequence, minHoursOnAfterMaxSequenceSaved) {
175
- return [
176
- {
177
- id: "n1",
178
- type: "power-saver",
179
- name: "test name",
180
- maxHoursToSaveInSequence,
181
- minHoursOnAfterMaxSequenceSaved,
182
- minSaving: 0.001,
183
- wires: [["n3"], ["n4"], ["n2"]],
184
- },
185
- { id: "n2", type: "helper" },
186
- { id: "n3", type: "helper" },
187
- { id: "n4", type: "helper" },
188
- ];
189
- }