node-red-contrib-power-saver 3.6.2 → 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 (105) 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 +17 -10
  5. package/docs/.vuepress/public/ads.txt +1 -0
  6. package/docs/README.md +4 -4
  7. package/docs/changelog/README.md +55 -1
  8. package/docs/contribute/README.md +8 -3
  9. package/docs/examples/example-grid-tariff-capacity-flow.json +23 -7
  10. package/docs/examples/example-grid-tariff-capacity-part.md +657 -22
  11. package/docs/faq/README.md +1 -1
  12. package/docs/faq/best-save-viewer.md +1 -1
  13. package/docs/guide/README.md +20 -5
  14. package/docs/images/best-save-config.png +0 -0
  15. package/docs/images/combine-two-lowest-price.png +0 -0
  16. package/docs/images/fixed-schedule-config.png +0 -0
  17. package/docs/images/global-context-window.png +0 -0
  18. package/docs/images/lowest-price-config.png +0 -0
  19. package/docs/images/node-ps-schedule-merger.png +0 -0
  20. package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
  21. package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
  22. package/docs/images/schedule-merger-config.png +0 -0
  23. package/docs/images/schedule-merger-example-1.png +0 -0
  24. package/docs/images/vipps-plakat.png +0 -0
  25. package/docs/images/vipps-qr.png +0 -0
  26. package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
  27. package/docs/nodes/README.md +12 -6
  28. package/docs/nodes/dynamic-commands.md +79 -0
  29. package/docs/nodes/dynamic-config.md +76 -0
  30. package/docs/nodes/ps-elvia-add-tariff.md +4 -0
  31. package/docs/nodes/ps-general-add-tariff.md +10 -0
  32. package/docs/nodes/ps-receive-price.md +2 -1
  33. package/docs/nodes/ps-schedule-merger.md +227 -0
  34. package/docs/nodes/ps-strategy-best-save.md +46 -110
  35. package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
  36. package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
  37. package/docs/nodes/ps-strategy-lowest-price.md +51 -112
  38. package/package.json +5 -2
  39. package/src/elvia/elvia-add-tariff.html +1 -2
  40. package/src/elvia/elvia-add-tariff.js +0 -1
  41. package/src/elvia/elvia-api.js +6 -0
  42. package/src/elvia/elvia-tariff.html +1 -1
  43. package/src/general-add-tariff.html +14 -8
  44. package/src/general-add-tariff.js +0 -1
  45. package/src/handle-input.js +94 -106
  46. package/src/handle-output.js +109 -0
  47. package/src/receive-price-functions.js +3 -3
  48. package/src/schedule-merger-functions.js +98 -0
  49. package/src/schedule-merger.html +135 -0
  50. package/src/schedule-merger.js +108 -0
  51. package/src/strategy-best-save.html +38 -1
  52. package/src/strategy-best-save.js +17 -63
  53. package/src/strategy-fixed-schedule.html +339 -0
  54. package/src/strategy-fixed-schedule.js +84 -0
  55. package/src/strategy-functions.js +35 -0
  56. package/src/strategy-lowest-price.html +76 -38
  57. package/src/strategy-lowest-price.js +16 -35
  58. package/src/utils.js +75 -2
  59. package/test/commands-input-best-save.test.js +142 -0
  60. package/test/commands-input-lowest-price.test.js +149 -0
  61. package/test/commands-input-schedule-merger.test.js +128 -0
  62. package/test/data/best-save-overlap-result.json +5 -1
  63. package/test/data/best-save-result.json +4 -0
  64. package/test/data/commands-result-best-save.json +383 -0
  65. package/test/data/commands-result-lowest-price.json +340 -0
  66. package/test/data/fixed-schedule-result.json +353 -0
  67. package/test/data/lowest-price-result-cont-max-fail.json +5 -1
  68. package/test/data/lowest-price-result-cont-max.json +3 -1
  69. package/test/data/lowest-price-result-cont.json +8 -1
  70. package/test/data/lowest-price-result-missing-end.json +8 -3
  71. package/test/data/lowest-price-result-neg-cont.json +27 -0
  72. package/test/data/lowest-price-result-neg-split.json +23 -0
  73. package/test/data/lowest-price-result-split-allday.json +3 -1
  74. package/test/data/lowest-price-result-split-allday10.json +1 -0
  75. package/test/data/lowest-price-result-split-max.json +3 -1
  76. package/test/data/lowest-price-result-split.json +3 -1
  77. package/test/data/merge-schedule-data.js +238 -0
  78. package/test/data/negative-prices.json +197 -0
  79. package/test/data/nordpool-event-prices.json +96 -480
  80. package/test/data/nordpool-zero-prices.json +90 -0
  81. package/test/data/reconfigResult.js +1 -0
  82. package/test/data/result.js +1 -0
  83. package/test/data/tibber-result-end-0-24h.json +12 -2
  84. package/test/data/tibber-result-end-0.json +12 -2
  85. package/test/data/tibber-result.json +1 -0
  86. package/test/receive-price.test.js +22 -0
  87. package/test/schedule-merger-functions.test.js +101 -0
  88. package/test/schedule-merger-test-utils.js +27 -0
  89. package/test/schedule-merger.test.js +130 -0
  90. package/test/send-config-input.test.js +45 -2
  91. package/test/strategy-best-save-test-utils.js +1 -1
  92. package/test/strategy-best-save.test.js +45 -0
  93. package/test/strategy-fixed-schedule.test.js +117 -0
  94. package/test/strategy-heat-capacitor.test.js +1 -1
  95. package/test/strategy-lowest-price-functions.test.js +1 -1
  96. package/test/strategy-lowest-price-test-utils.js +31 -0
  97. package/test/strategy-lowest-price.test.js +55 -45
  98. package/test/test-utils.js +43 -36
  99. package/test/utils.test.js +13 -0
  100. package/docs/images/node-power-saver.png +0 -0
  101. package/docs/nodes/power-saver.md +0 -23
  102. package/src/power-saver.html +0 -116
  103. package/src/power-saver.js +0 -260
  104. package/test/commands-input.test.js +0 -47
  105. package/test/power-saver.test.js +0 -189
@@ -0,0 +1,84 @@
1
+ const {
2
+ booleanConfig,
3
+ calcNullSavings,
4
+ fixOutputValues,
5
+ fixPeriods,
6
+ getSavings,
7
+ saveOriginalConfig,
8
+ } = require("./utils");
9
+ const { strategyOnInput } = require("./strategy-functions");
10
+ const { DateTime } = require("luxon");
11
+ const cloneDeep = require("lodash.clonedeep");
12
+
13
+ module.exports = function (RED) {
14
+ function StrategyFixedScheduleNode(config) {
15
+ RED.nodes.createNode(this, config);
16
+ const node = this;
17
+ node.status({});
18
+
19
+ const validConfig = {
20
+ periods: config.periods || [],
21
+ validFrom: config.validFrom,
22
+ validTo: config.validTo,
23
+ days: config.days || { Mon: true, Tue: true, Wed: true, Thu: true, Fri: true, Sat: true, Sun: true },
24
+ contextStorage: config.contextStorage || "default",
25
+ outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule),
26
+ outputValueForOn: config.outputValueForOn || true,
27
+ outputValueForOff: config.outputValueForOff || false,
28
+ outputValueForOntype: config.outputValueForOntype || "bool",
29
+ outputValueForOfftype: config.outputValueForOfftype || "bool",
30
+ override: "auto",
31
+ sendCurrentValueWhenRescheduling: config.sendCurrentValueWhenRescheduling,
32
+ };
33
+
34
+ fixOutputValues(validConfig);
35
+ fixPeriods(validConfig);
36
+ saveOriginalConfig(node, validConfig);
37
+
38
+ node.on("close", function () {
39
+ clearTimeout(node.schedulingTimeout);
40
+ });
41
+
42
+ node.on("input", function (msg) {
43
+ strategyOnInput(node, msg, doPlanning, calcNullSavings);
44
+ });
45
+ }
46
+ RED.nodes.registerType("ps-strategy-fixed-schedule", StrategyFixedScheduleNode);
47
+ };
48
+
49
+ function doPlanning(node, priceData) {
50
+ const startTimes = priceData.map((pd) => pd.start);
51
+ const onOff = startTimes.map(() => node.outputIfNoSchedule);
52
+ const allHours = buildAllHours(node, node.periods);
53
+ const validFrom = DateTime.fromISO(node.validFrom || startTimes[0].substr(0, 10));
54
+ const validTo = DateTime.fromISO(node.validTo || startTimes[startTimes.length - 1].substr(0, 10));
55
+ startTimes.forEach((st, i) => {
56
+ const date = DateTime.fromISO(st.substr(0, 10));
57
+ const hour = DateTime.fromISO(st).hour;
58
+ const day = DateTime.fromISO(st).weekday;
59
+ const dayName = Object.keys(node.days)[day - 1];
60
+ if (date >= validFrom && date <= validTo && node.days[dayName]) {
61
+ onOff[i] = allHours[hour];
62
+ }
63
+ });
64
+
65
+ return onOff;
66
+ }
67
+
68
+ function buildAllHours(node, periods) {
69
+ const sortedPeriods = cloneDeep(periods);
70
+ sortedPeriods.sort((a, b) => a.start - b.start);
71
+ let res = [];
72
+ let hour = 0;
73
+ let current = sortedPeriods[sortedPeriods.length - 1];
74
+ sortedPeriods.push({ start: 24, value: null });
75
+ sortedPeriods.forEach((period) => {
76
+ const nextHour = parseInt(period.start);
77
+ while (hour < nextHour) {
78
+ res[hour] = current.value;
79
+ hour++;
80
+ }
81
+ current = period;
82
+ });
83
+ return res;
84
+ }
@@ -0,0 +1,35 @@
1
+ const { DateTime } = require("luxon");
2
+ const { getEffectiveConfig, getOutputForTime } = require("./utils");
3
+ const { handleStrategyInput } = require("./handle-input");
4
+ const { handleOutput, shallSendOutput, strategyShallSendSchedule } = require("./handle-output");
5
+
6
+ function strategyOnInput(node, msg, doPlanning, calcSavings) {
7
+ if (msg.payload?.name && msg.payload.name !== node.name) {
8
+ // If payload.name is set, and does not match this nodes name, discard message
9
+ return;
10
+ }
11
+ const config = getEffectiveConfig(node, msg);
12
+ const { plan, commands } = handleStrategyInput(node, msg, config, doPlanning, calcSavings);
13
+ if (plan) {
14
+ const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
15
+ const currentOutput = node.context().get("currentOutput");
16
+ const plannedOutputNow =
17
+ node.override === "auto"
18
+ ? getOutputForTime(plan.schedule, planFromTime, node.outputIfNoSchedule)
19
+ : node.override === "on";
20
+ const outputCommands = {
21
+ sendOutput: shallSendOutput(
22
+ msg,
23
+ commands,
24
+ currentOutput,
25
+ plannedOutputNow,
26
+ node.sendCurrentValueWhenRescheduling
27
+ ),
28
+ sendSchedule: strategyShallSendSchedule(msg, commands),
29
+ runSchedule: commands.replan !== false,
30
+ };
31
+ handleOutput(node, config, plan, outputCommands, planFromTime);
32
+ }
33
+ }
34
+
35
+ module.exports = { strategyOnInput };
@@ -61,6 +61,24 @@
61
61
  required: true,
62
62
  align: "left",
63
63
  },
64
+ outputValueForOn: {
65
+ value: true,
66
+ required: true,
67
+ validate: RED.validators.typedInput("outputValueForOntype", false),
68
+ },
69
+ outputValueForOff: {
70
+ value: false,
71
+ required: true,
72
+ validate: RED.validators.typedInput("outputValueForOfftype", false),
73
+ },
74
+ outputValueForOntype: {
75
+ value: "bool",
76
+ required: true,
77
+ },
78
+ outputValueForOfftype: {
79
+ value: "bool",
80
+ required: true,
81
+ },
64
82
  outputIfNoSchedule: { value: "true", required: true, align: "left" },
65
83
  outputOutsidePeriod: { value: "false", required: true, align: "left" },
66
84
  contextStorage: { value: "default", required: false, align: "left" },
@@ -132,58 +150,78 @@
132
150
  },
133
151
  ],
134
152
  });
153
+ $("#node-input-outputValueForOn").typedInput({
154
+ default: "bool",
155
+ typeField: $("#node-input-outputValueForOntype"),
156
+ types: ["bool", "num", "str"],
157
+ });
158
+ $("#node-input-outputValueForOff").typedInput({
159
+ default: "bool",
160
+ typeField: $("#node-input-outputValueForOfftype"),
161
+ types: ["bool", "num", "str"],
162
+ });
135
163
  },
136
164
  });
137
165
  </script>
138
166
 
139
167
  <script type="text/html" data-template-name="ps-strategy-lowest-price">
140
- <div class="form-row">
141
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
142
- <input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
143
- </div>
144
- <div class="form-row">
145
- <label for="node-input-fromTime"><i class="fa fa-clock-o"></i> From time</label>
146
- <input type="text" id="node-input-fromTime" style="width: 80px">
168
+ <div class="form-row">
169
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
170
+ <input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
147
171
  </div>
148
- <div class="form-row">
149
- <label for="node-input-toTime"><i class="fa fa-clock-o"></i> To time</label>
150
- <input type="text" id="node-input-toTime" style="width: 80px">
151
- </div>
152
- <div class="form-row">
153
- <label for="node-input-hoursOn"><i class="fa fa-arrows-h"></i> Hours on</label>
154
- <input type="text" id="node-input-hoursOn" style="width: 80px">
155
- </div>
156
- <div class="form-row">
157
- <label for="node-input-maxPrice"><i class="fa fa-minus"></i> Max price</label>
158
- <input type="text" id="node-input-maxPrice" placeholder="Max price" style="width: 80px">
159
- </div>
160
- <div class="form-row">
161
- <label for="node-input-doNotSplit">Consecutive on-period</label>
162
- <input type="checkbox" id="node-input-doNotSplit" style="display:inline-block; width:22px; vertical-align:top;">
163
- </label>
164
- </div>
165
172
  <div class="form-row">
166
- <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
167
- <input type="checkbox"
168
- id="node-input-sendCurrentValueWhenRescheduling"
169
- style="display:inline-block; width:22px; vertical-align:top;"
170
- autocomplete="off"><span>Send when rescheduling</span>
171
- </label>
173
+ <label for="node-input-fromTime"><i class="fa fa-clock-o"></i> From time</label>
174
+ <input type="text" id="node-input-fromTime" style="width: 80px">
175
+ </div>
176
+ <div class="form-row">
177
+ <label for="node-input-toTime"><i class="fa fa-clock-o"></i> To time</label>
178
+ <input type="text" id="node-input-toTime" style="width: 80px">
172
179
  </div>
173
180
  <div class="form-row">
174
- <label for="node-input-outputIfNoSchedule">If no schedule, send</label>
175
- <input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
176
- </label>
181
+ <label for="node-input-hoursOn"><i class="fa fa-arrows-h"></i> Hours on</label>
182
+ <input type="text" id="node-input-hoursOn" style="width: 80px">
177
183
  </div>
178
184
  <div class="form-row">
179
- <label for="node-input-outputIfNoSchedule">Outside period, send</label>
180
- <input type="text" id="node-input-outputOutsidePeriod" style="width: 80px">
181
- </label>
185
+ <label for="node-input-maxPrice"><i class="fa fa-minus"></i> Max price</label>
186
+ <input type="text" id="node-input-maxPrice" placeholder="Max price" style="width: 80px">
182
187
  </div>
183
188
  <div class="form-row">
184
- <label for="node-input-contextStorage"><i class="fa fa-archive"></i> Context storage</label>
185
- <input type="text" id="node-input-contextStorage" style="width: 160px">
189
+ <label for="node-input-doNotSplit">Consecutive on-period</label>
190
+ <input type="checkbox" id="node-input-doNotSplit" style="display:inline-block; width:22px; vertical-align:top;">
191
+ </label>
186
192
  </div>
193
+ <div class="form-row">
194
+ <label for="node-input-outputValueForOn">Output value for on</label>
195
+ <input type="text" id="node-input-outputValueForOn" style="text-align: left; width: 120px">
196
+ <input type="hidden" id="node-input-outputValueForOntype">
197
+ </div>
198
+ <div class="form-row">
199
+ <label for="node-input-outputValueForOff">Output value for off</label>
200
+ <input type="text" id="node-input-outputValueForOff" style="text-align: left; width: 120px">
201
+ <input type="hidden" id="node-input-outputValueForOfftype">
202
+ </div>
203
+ <div class="form-row">
204
+ <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
205
+ <input type="checkbox"
206
+ id="node-input-sendCurrentValueWhenRescheduling"
207
+ style="display:inline-block; width:22px; vertical-align:top;"
208
+ autocomplete="off"><span>Send when rescheduling</span>
209
+ </label>
210
+ </div>
211
+ <div class="form-row">
212
+ <label for="node-input-outputIfNoSchedule">If no schedule, send</label>
213
+ <input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
214
+ </label>
215
+ </div>
216
+ <div class="form-row">
217
+ <label for="node-input-outputIfNoSchedule">Outside period, send</label>
218
+ <input type="text" id="node-input-outputOutsidePeriod" style="width: 80px">
219
+ </label>
220
+ </div>
221
+ <div class="form-row">
222
+ <label for="node-input-contextStorage"><i class="fa fa-archive"></i> Context storage</label>
223
+ <input type="text" id="node-input-contextStorage" style="width: 160px">
224
+ </div>
187
225
  </script>
188
226
 
189
227
  <script type="text/markdown" data-help-name="ps-strategy-lowest-price">
@@ -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
+ });