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,135 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("ps-schedule-merger", {
3
+ category: "Power Saver",
4
+ color: "#a6bbcf",
5
+ defaults: {
6
+ name: { value: "Schedule Merger" },
7
+ outputIfNoSchedule: {
8
+ value: true,
9
+ required: true,
10
+ align: "left",
11
+ },
12
+ logicFunction: { value: "OR", required: true, align: "left" },
13
+ schedulingDelay: {
14
+ value: 2000,
15
+ required: true,
16
+ validate: RED.validators.number(),
17
+ },
18
+ sendCurrentValueWhenRescheduling: {
19
+ value: true,
20
+ required: true,
21
+ align: "left",
22
+ },
23
+ outputValueForOn: {
24
+ value: true,
25
+ required: true,
26
+ validate: RED.validators.typedInput("outputValueForOntype", false),
27
+ },
28
+ outputValueForOff: {
29
+ value: false,
30
+ required: true,
31
+ validate: RED.validators.typedInput("outputValueForOfftype", false),
32
+ },
33
+ outputValueForOntype: {
34
+ value: "bool",
35
+ required: true,
36
+ },
37
+ outputValueForOfftype: {
38
+ value: "bool",
39
+ required: true,
40
+ },
41
+ },
42
+ inputs: 1,
43
+ outputs: 3,
44
+ icon: "font-awesome/fa-compress",
45
+ color: "#FFCC66",
46
+ label: function () {
47
+ return this.name || "Best Save";
48
+ },
49
+ outputLabels: ["on", "off", "schedule"],
50
+ oneditprepare: function () {
51
+ $("#node-input-outputIfNoSchedule").typedInput({
52
+ types: [
53
+ {
54
+ value: "onoff",
55
+ options: [
56
+ { value: "true", label: "On" },
57
+ { value: "false", label: "Off" },
58
+ ],
59
+ },
60
+ ],
61
+ });
62
+ $("#node-input-logicFunction").typedInput({
63
+ types: [
64
+ {
65
+ value: "logic",
66
+ options: ["OR", "AND"],
67
+ },
68
+ ],
69
+ });
70
+ $("#node-input-outputValueForOn").typedInput({
71
+ default: "bool",
72
+ typeField: $("#node-input-outputValueForOntype"),
73
+ types: ["bool", "num", "str"],
74
+ });
75
+ $("#node-input-outputValueForOff").typedInput({
76
+ default: "bool",
77
+ typeField: $("#node-input-outputValueForOfftype"),
78
+ types: ["bool", "num", "str"],
79
+ });
80
+ },
81
+ });
82
+ </script>
83
+
84
+ <script type="text/html" data-template-name="ps-schedule-merger">
85
+ <div class="form-row">
86
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
87
+ <input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
88
+ </div>
89
+ <div class="form-row">
90
+ <label for="node-input-logicFunction">Function</label>
91
+ <input type="text" id="node-input-logicFunction" style="width: 80px">
92
+ </label>
93
+ </div>
94
+ <div class="form-row">
95
+ <label for="node-input-schedulingDelay"><i class="fa fa-clock-o"></i> Delay</label>
96
+ <input type="text" id="node-input-schedulingDelay" placeholder="milliseconds" style="width: 80px">
97
+ milliseconds
98
+ </div>
99
+ <div class="form-row">
100
+ <label for="node-input-outputValueForOn">Output value for on</label>
101
+ <input type="text" id="node-input-outputValueForOn" style="text-align: left; width: 120px">
102
+ <input type="hidden" id="node-input-outputValueForOntype">
103
+ </div>
104
+ <div class="form-row">
105
+ <label for="node-input-outputValueForOff">Output value for off</label>
106
+ <input type="text" id="node-input-outputValueForOff" style="text-align: left; width: 120px">
107
+ <input type="hidden" id="node-input-outputValueForOfftype">
108
+ </div>
109
+ </div>
110
+ <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
111
+ <input type="checkbox"
112
+ id="node-input-sendCurrentValueWhenRescheduling"
113
+ style="display:inline-block; width:22px; vertical-align:top;"
114
+ autocomplete="off"><span>Send when rescheduling</span>
115
+ </label>
116
+ </div>
117
+ <div class="form-row">
118
+ <label for="node-input-outputIfNoSchedule">If no schedule, send</label>
119
+ <input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
120
+ </label>
121
+ </div>
122
+ </script>
123
+
124
+ <script type="text/markdown" data-help-name="ps-schedule-merger">
125
+ A node you can use to merge multiple schedules to one.
126
+
127
+ Function OR will turn on if any of the input schedules are on.
128
+
129
+ Function AND will turn on only when all input schedules are on.
130
+
131
+ Delay is milliseconds before the merged schedule is sent. It is useful to wait for all schedules
132
+ to arrive before they are merged.
133
+
134
+ Please read more in the [node documentation](https://powersaver.no/nodes/schedule-merger)
135
+ </script>
@@ -0,0 +1,108 @@
1
+ const {
2
+ msgHasSchedule,
3
+ mergeSchedules,
4
+ saveSchedule,
5
+ validateSchedule,
6
+ mergerShallSendOutput,
7
+ mergerShallSendSchedule,
8
+ } = require("./schedule-merger-functions.js");
9
+ const {
10
+ booleanConfig,
11
+ fixOutputValues,
12
+ getEffectiveConfig,
13
+ getOutputForTime,
14
+ makeScheduleFromHours,
15
+ saveOriginalConfig,
16
+ } = require("./utils.js");
17
+ const { DateTime } = require("luxon");
18
+ const nanoTime = require("nano-time");
19
+ const { handleOutput } = require("./handle-output");
20
+ const { addLastSwitchIfNoSchedule, getCommands } = require("./handle-input");
21
+
22
+ module.exports = function (RED) {
23
+ function ScheduleMerger(config) {
24
+ RED.nodes.createNode(this, config);
25
+ const node = this;
26
+ node.status({});
27
+
28
+ const validConfig = {
29
+ logicFunction: config.logicFunction,
30
+ schedulingDelay: config.schedulingDelay || 2000,
31
+ sendCurrentValueWhenRescheduling: config.sendCurrentValueWhenRescheduling,
32
+ outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule),
33
+ outputValueForOn: config.outputValueForOn || true,
34
+ outputValueForOff: config.outputValueForOff || false,
35
+ outputValueForOntype: config.outputValueForOntype || "bool",
36
+ outputValueForOfftype: config.outputValueForOfftype || "bool",
37
+ override: "auto",
38
+ };
39
+
40
+ fixOutputValues(validConfig);
41
+ saveOriginalConfig(node, validConfig);
42
+
43
+ node.on("close", function () {
44
+ clearTimeout(node.schedulingTimeout);
45
+ });
46
+
47
+ node.on("input", function (msg) {
48
+ if (msg.payload.hours) {
49
+ // Delete config from strategy nodes so it does not merge
50
+ // with config for this node.
51
+ delete msg.payload.config;
52
+ }
53
+ const config = getEffectiveConfig(node, msg);
54
+ const commands = getCommands(msg);
55
+ const myTime = nanoTime();
56
+ if (msgHasSchedule(msg)) {
57
+ const validationError = validateSchedule(msg);
58
+ if (validationError) {
59
+ node.warn(validationError);
60
+ node.status({ fill: "red", shape: "dot", text: validationError });
61
+ return;
62
+ }
63
+ saveSchedule(node, msg);
64
+ // Wait for more schedules to arrive before proceeding
65
+ node.lastSavedScheduleTime = myTime;
66
+ }
67
+
68
+ setTimeout(
69
+ () => {
70
+ if (node.lastSavedScheduleTime !== myTime && msgHasSchedule(msg) && !commands.replan) {
71
+ // Another schedule has arrived later
72
+ return;
73
+ }
74
+
75
+ const hours = mergeSchedules(node, node.logicFunction);
76
+ const schedule = makeScheduleFromHours(hours);
77
+ addLastSwitchIfNoSchedule(schedule, hours, node);
78
+
79
+ const plan = {
80
+ hours,
81
+ schedule,
82
+ source: node.name,
83
+ };
84
+
85
+ const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
86
+ const currentOutput = node.context().get("currentOutput");
87
+ const plannedOutputNow = getOutputForTime(plan.schedule, planFromTime, node.outputIfNoSchedule);
88
+
89
+ const outputCommands = {
90
+ sendOutput: mergerShallSendOutput(
91
+ msg,
92
+ commands,
93
+ currentOutput,
94
+ plannedOutputNow,
95
+ node.sendCurrentValueWhenRescheduling
96
+ ),
97
+ sendSchedule: mergerShallSendSchedule(msg, commands),
98
+ runSchedule: commands.runSchedule || (commands.runSchedule !== false && msgHasSchedule(msg)),
99
+ };
100
+
101
+ handleOutput(node, config, plan, outputCommands, planFromTime);
102
+ },
103
+ commands.replan ? 0 : node.schedulingDelay
104
+ );
105
+ });
106
+ }
107
+ RED.nodes.registerType("ps-schedule-merger", ScheduleMerger);
108
+ };
@@ -22,9 +22,26 @@
22
22
  sendCurrentValueWhenRescheduling: {
23
23
  value: true,
24
24
  required: true,
25
- // validate: RED.validators.number(),
26
25
  align: "left",
27
26
  },
27
+ outputValueForOn: {
28
+ value: true,
29
+ required: true,
30
+ validate: RED.validators.typedInput("outputValueForOntype", false),
31
+ },
32
+ outputValueForOff: {
33
+ value: false,
34
+ required: true,
35
+ validate: RED.validators.typedInput("outputValueForOfftype", false),
36
+ },
37
+ outputValueForOntype: {
38
+ value: "bool",
39
+ required: true,
40
+ },
41
+ outputValueForOfftype: {
42
+ value: "bool",
43
+ required: true,
44
+ },
28
45
  outputIfNoSchedule: { value: "true", required: true, align: "left" },
29
46
  contextStorage: { value: "default", required: false, align: "left" },
30
47
  },
@@ -56,6 +73,16 @@
56
73
  },
57
74
  ],
58
75
  });
76
+ $("#node-input-outputValueForOn").typedInput({
77
+ default: "bool",
78
+ typeField: $("#node-input-outputValueForOntype"),
79
+ types: ["bool", "num", "str"],
80
+ });
81
+ $("#node-input-outputValueForOff").typedInput({
82
+ default: "bool",
83
+ typeField: $("#node-input-outputValueForOfftype"),
84
+ types: ["bool", "num", "str"],
85
+ });
59
86
  },
60
87
  });
61
88
  </script>
@@ -83,6 +110,16 @@
83
110
  </div>
84
111
  <h3>Output</h3>
85
112
  <div class="form-row">
113
+ <div class="form-row">
114
+ <label for="node-input-outputValueForOn">Output value for on</label>
115
+ <input type="text" id="node-input-outputValueForOn" style="text-align: left; width: 120px">
116
+ <input type="hidden" id="node-input-outputValueForOntype">
117
+ </div>
118
+ <div class="form-row">
119
+ <label for="node-input-outputValueForOff">Output value for off</label>
120
+ <input type="text" id="node-input-outputValueForOff" style="text-align: left; width: 120px">
121
+ <input type="hidden" id="node-input-outputValueForOfftype">
122
+ </div>
86
123
  <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
87
124
  <input type="checkbox"
88
125
  id="node-input-sendCurrentValueWhenRescheduling"
@@ -1,7 +1,6 @@
1
- const { countAtEnd, makeSchedule, getSavings, getDiff } = require("./utils");
2
- const { handleStrategyInput } = require("./handle-input");
3
- const { loadDayData } = require("./utils");
1
+ const { booleanConfig, fixOutputValues, getSavings, saveOriginalConfig } = require("./utils");
4
2
  const mostSavedStrategy = require("./strategy-best-save-functions");
3
+ const { strategyOnInput } = require("./strategy-functions");
5
4
 
6
5
  module.exports = function (RED) {
7
6
  function StrategyBestSaveNode(config) {
@@ -9,86 +8,41 @@ module.exports = function (RED) {
9
8
  const node = this;
10
9
  node.status({});
11
10
 
12
- const originalConfig = {
11
+ const validConfig = {
12
+ contextStorage: config.contextStorage || "default",
13
13
  maxHoursToSaveInSequence: config.maxHoursToSaveInSequence,
14
14
  minHoursOnAfterMaxSequenceSaved: config.minHoursOnAfterMaxSequenceSaved,
15
15
  minSaving: parseFloat(config.minSaving),
16
+ outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule),
17
+ outputValueForOn: config.outputValueForOn || true,
18
+ outputValueForOff: config.outputValueForOff || false,
19
+ outputValueForOntype: config.outputValueForOntype || "bool",
20
+ outputValueForOfftype: config.outputValueForOfftype || "bool",
21
+ override: "auto",
16
22
  sendCurrentValueWhenRescheduling: config.sendCurrentValueWhenRescheduling,
17
- outputIfNoSchedule: config.outputIfNoSchedule === "true",
18
- contextStorage: config.contextStorage || "default",
19
23
  };
20
- node.context().set("config", originalConfig);
21
- node.contextStorage = originalConfig.contextStorage;
24
+
25
+ fixOutputValues(validConfig);
26
+ saveOriginalConfig(node, validConfig);
22
27
 
23
28
  node.on("close", function () {
24
29
  clearTimeout(node.schedulingTimeout);
25
30
  });
26
31
 
27
32
  node.on("input", function (msg) {
28
- handleStrategyInput(node, msg, doPlanning);
33
+ strategyOnInput(node, msg, doPlanning, getSavings);
29
34
  });
30
35
  }
31
36
  RED.nodes.registerType("ps-strategy-best-save", StrategyBestSaveNode);
32
37
  };
33
38
 
34
- function adjustSavingsPassedHours(plan, includeFromLastPlanHours) {
35
- const firstOnIndex = plan.hours.findIndex((h) => h.onOff);
36
- if (firstOnIndex < 0) {
37
- return;
38
- }
39
- const nextOnValue = plan.hours[firstOnIndex].price;
40
- let adjustIndex = includeFromLastPlanHours.length - 1;
41
- while (adjustIndex >= 0 && !includeFromLastPlanHours[adjustIndex].onOff) {
42
- includeFromLastPlanHours[adjustIndex].saving = getDiff(includeFromLastPlanHours[adjustIndex].price, nextOnValue);
43
- adjustIndex--;
44
- }
45
- }
46
-
47
- function loadDataJustBefore(node, dateDayBefore) {
48
- const dataDayBefore = loadDayData(node, dateDayBefore);
49
- return {
50
- schedule: [...dataDayBefore.schedule],
51
- hours: [...dataDayBefore.hours],
52
- };
53
- }
54
-
55
- function doPlanning(node, _, priceData, _, dateDayBefore, _) {
56
- const dataJustBefore = loadDataJustBefore(node, dateDayBefore);
39
+ function doPlanning(node, priceData) {
57
40
  const values = priceData.map((d) => d.value);
58
- const startTimes = priceData.map((d) => d.start);
59
- const onOffBefore = dataJustBefore.hours.map((h) => h.onOff);
60
- const lastPlanHours = node.context().get("lastPlan", node.contextStorage)?.hours ?? [];
61
- const plan = makePlan(node, values, startTimes, onOffBefore);
62
- const includeFromLastPlanHours = lastPlanHours.filter(
63
- (h) => h.start < plan.hours[0].start && h.start >= priceData[0].start
64
- );
65
- adjustSavingsPassedHours(plan, includeFromLastPlanHours);
66
- plan.hours.splice(0, 0, ...includeFromLastPlanHours);
67
- return plan;
68
- }
69
-
70
- function makePlan(node, values, startTimes, onOffBefore, firstValueNextDay) {
71
- const lastValueDayBefore = onOffBefore[onOffBefore.length - 1];
72
- const lastCountDayBefore = countAtEnd(onOffBefore, lastValueDayBefore);
73
41
  const onOff = mostSavedStrategy.calculate(
74
42
  values,
75
43
  node.maxHoursToSaveInSequence,
76
44
  node.minHoursOnAfterMaxSequenceSaved,
77
- node.minSaving,
78
- lastValueDayBefore,
79
- lastCountDayBefore
45
+ node.minSaving
80
46
  );
81
-
82
- const schedule = makeSchedule(onOff, startTimes, lastValueDayBefore);
83
- const savings = getSavings(values, onOff, firstValueNextDay);
84
- const hours = values.map((v, i) => ({
85
- price: v,
86
- onOff: onOff[i],
87
- start: startTimes[i],
88
- saving: savings[i],
89
- }));
90
- return {
91
- hours,
92
- schedule,
93
- };
47
+ return onOff;
94
48
  }