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,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
  }
@@ -0,0 +1,339 @@
1
+ <script type="text/javascript">
2
+ const defaultDaysSfs = { Mon: true, Tue: true, Wed: true, Thu: true, Fri: true, Sat: true, Sun: true };
3
+ RED.nodes.registerType("ps-strategy-fixed-schedule", {
4
+ category: "Power Saver",
5
+ color: "#a6bbcf",
6
+ defaults: {
7
+ name: { value: "Fixed Schedule" },
8
+ periods: {
9
+ value: [{ start: "00", value: true }],
10
+ validate: function () {
11
+ return !this.periods.some((p) => !/^(true)|(false)$/.test("" + p.value));
12
+ },
13
+ },
14
+ validFrom: {
15
+ value: null,
16
+ required: false,
17
+ validate: RED.validators.regex(/^$|^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$/),
18
+ },
19
+ validTo: {
20
+ value: null,
21
+ required: false,
22
+ validate: RED.validators.regex(/^$|^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$/),
23
+ },
24
+ days: { value: { ...defaultDaysSfs } },
25
+ sendCurrentValueWhenRescheduling: {
26
+ value: true,
27
+ required: true,
28
+ align: "left",
29
+ },
30
+ outputValueForOn: {
31
+ value: true,
32
+ required: true,
33
+ validate: RED.validators.typedInput("outputValueForOntype", false),
34
+ },
35
+ outputValueForOff: {
36
+ value: false,
37
+ required: true,
38
+ validate: RED.validators.typedInput("outputValueForOfftype", false),
39
+ },
40
+ outputValueForOntype: {
41
+ value: "bool",
42
+ required: true,
43
+ },
44
+ outputValueForOfftype: {
45
+ value: "bool",
46
+ required: true,
47
+ },
48
+ outputIfNoSchedule: { value: "false", required: true, align: "left" },
49
+ contextStorage: { value: "default", required: false, align: "left" },
50
+ },
51
+ hours: [
52
+ "00",
53
+ "01",
54
+ "02",
55
+ "03",
56
+ "04",
57
+ "05",
58
+ "06",
59
+ "07",
60
+ "08",
61
+ "09",
62
+ "10",
63
+ "11",
64
+ "12",
65
+ "13",
66
+ "14",
67
+ "15",
68
+ "16",
69
+ "17",
70
+ "18",
71
+ "19",
72
+ "20",
73
+ "21",
74
+ "22",
75
+ "23",
76
+ ],
77
+ inputs: 1,
78
+ outputs: 3,
79
+ icon: "font-awesome/fa-bar-chart",
80
+ color: "#FFCC66",
81
+ label: function () {
82
+ return this.name || "Fixed Schedule";
83
+ },
84
+ outputLabels: ["on", "off", "schedule"],
85
+ oneditprepare: function () {
86
+ $("#node-input-outputIfNoSchedule").typedInput({
87
+ types: [
88
+ {
89
+ value: "onoff",
90
+ options: [
91
+ { value: "true", label: "On" },
92
+ { value: "false", label: "Off" },
93
+ ],
94
+ },
95
+ ],
96
+ });
97
+ $("#node-input-contextStorage").typedInput({
98
+ types: [
99
+ {
100
+ value: "storages",
101
+ options: RED.settings.context.stores.map((s) => ({ value: s, label: s })),
102
+ },
103
+ ],
104
+ });
105
+ $("#node-input-outputValueForOn").typedInput({
106
+ default: "bool",
107
+ typeField: $("#node-input-outputValueForOntype"),
108
+ types: ["bool", "num", "str"],
109
+ });
110
+ $("#node-input-outputValueForOff").typedInput({
111
+ default: "bool",
112
+ typeField: $("#node-input-outputValueForOfftype"),
113
+ types: ["bool", "num", "str"],
114
+ });
115
+ const createElement = function (type, attrs = [], children = []) {
116
+ const el = document.createElement(type);
117
+ attrs.forEach((attr) => {
118
+ el.setAttribute(attr[0], attr[1]);
119
+ });
120
+ children.forEach((child) => {
121
+ el.append(child);
122
+ });
123
+ return el;
124
+ };
125
+
126
+ const createInputPart = function (name, i, text, inpStyle, value) {
127
+ const id = `node-input-${name}-${i}`;
128
+ const label = createElement(
129
+ "label",
130
+ [
131
+ ["for", id],
132
+ ["style", "margin-right: 10px;"],
133
+ ],
134
+ []
135
+ );
136
+ label.innerHTML = text;
137
+ const inp = createElement("input", [
138
+ ["type", "text"],
139
+ ["id", id],
140
+ ["style", `width: 80px; ${inpStyle};`],
141
+ ]);
142
+
143
+ inp.value = value;
144
+ return createElement("span", [["style", "text-align: right;"]], [label, inp]);
145
+ };
146
+
147
+ const addPeriod = function (periods) {
148
+ const prev = periods[periods.length - 1].start;
149
+ const next = prev === "23" ? "00" : "" + (parseInt(prev) + 1);
150
+ periods.push({ start: next, value: null });
151
+ drawPeriods(periods);
152
+ };
153
+
154
+ const removePeriod = function (periods, i) {
155
+ periods.splice(i, 1);
156
+ drawPeriods(periods);
157
+ RED.nodes.dirty(true);
158
+ };
159
+
160
+ const drawPeriods = function (periods) {
161
+ document.getElementById("node-input-period-container").replaceChildren();
162
+ for (let i = 0; i < periods.length; i++) {
163
+ let period = periods[i];
164
+
165
+ const timeEl = createInputPart("fromTime", i, "From time:", "margin-right: 20px;", period.start);
166
+ const valEl = createInputPart("value", i, "Value:", "margin-right: 20px;", period.value);
167
+
168
+ let li;
169
+ if (periods.length > 1) {
170
+ // Delete button
171
+ const delButton = document.createElement("button");
172
+ delButton.setAttribute("style", "width: 24px;");
173
+ delButton.innerText = "X";
174
+ delButton.addEventListener("click", () => {
175
+ removePeriod(periods, i);
176
+ setTypedInputOnValues(periods.length);
177
+ });
178
+ li = createElement("div", [["style", "text-align: left;"]], [timeEl, valEl, delButton]);
179
+ } else {
180
+ li = createElement("div", [["style", "text-align: left;"]], [timeEl, valEl]);
181
+ }
182
+ $("#node-input-period-container").append(li);
183
+
184
+ $("#node-input-fromTime-" + i).typedInput({
185
+ types: [
186
+ {
187
+ value: "fromTime",
188
+ options: hours.map((h) => ({ value: h, label: h + ":00" })),
189
+ },
190
+ ],
191
+ });
192
+ $("#node-input-fromTime-" + i).change(function () {
193
+ periods[i].start = this.value;
194
+ RED.nodes.dirty(true);
195
+ });
196
+ $("#node-input-value-" + i).change(function () {
197
+ periods[i].value = this.value;
198
+ RED.nodes.dirty(true);
199
+ });
200
+ }
201
+ };
202
+
203
+ const drawDays = function (days) {
204
+ const dayNames = Object.keys(days);
205
+ document.getElementById("node-input-days-container").replaceChildren();
206
+ for (let i = 0; i < dayNames.length; i++) {
207
+ let day = dayNames[i];
208
+
209
+ const id = `node-input-day-${i}`;
210
+ const label = createElement(
211
+ "label",
212
+ [
213
+ ["for", id],
214
+ ["style", " margin: 4px 10px 0px 2px;width: 30px; text-align: left;"],
215
+ ],
216
+ []
217
+ );
218
+ label.innerHTML = day;
219
+ const attrs = [
220
+ ["name", "node-input-day"],
221
+ ["type", "checkbox"],
222
+ ["id", id],
223
+ ["style", `width: 15px; margin: 2px 0px 5px 5px;`],
224
+ ];
225
+ if (days[day]) {
226
+ attrs.push(["checked", ""]);
227
+ }
228
+ const inp = createElement("input", attrs);
229
+ inp.value = dayNames[i];
230
+
231
+ const el = createElement("span", [["style", "text-align: right;"]], [inp, label]);
232
+
233
+ $("#node-input-days-container").append(el);
234
+
235
+ $("#node-input-day-" + i).change(function (e) {
236
+ days[day] = $(this).is(":checked");
237
+ RED.nodes.dirty(true);
238
+ });
239
+ }
240
+ };
241
+
242
+ const setTypedInputOnValues = function (length) {
243
+ for (let i = 0; i < length; i++) {
244
+ $("#node-input-value-" + i).typedInput({
245
+ types: [
246
+ {
247
+ value: "onoff",
248
+ options: [
249
+ { value: "true", label: "On" },
250
+ { value: "false", label: "Off" },
251
+ ],
252
+ },
253
+ ],
254
+ });
255
+ }
256
+ };
257
+
258
+ drawPeriods(this.periods);
259
+ $("#add-period-button").on("click", () => {
260
+ addPeriod(this.periods);
261
+ setTypedInputOnValues(this.periods.length);
262
+ });
263
+
264
+ // Set typed input for value on all periods
265
+ setTypedInputOnValues(this.periods.length);
266
+
267
+ if (!this.days) {
268
+ // To support nodes created before this was developed
269
+ this.days = { ...defaultDaysSfs };
270
+ }
271
+ drawDays(this.days);
272
+ },
273
+ });
274
+ </script>
275
+
276
+ <script type="text/html" data-template-name="ps-strategy-fixed-schedule">
277
+ <div class="form-row">
278
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
279
+ <input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
280
+ </div>
281
+ <h3>Schedule</h3>
282
+ <div class="form-row node-input-period-container-row">
283
+ <div id="node-input-period-container"></div>
284
+ </div>
285
+
286
+ <div class="form-row">
287
+ <button type="button" id="add-period-button" class="red-ui-button">Add period</button>
288
+ </div>
289
+
290
+ <div class="form-row">
291
+ <label for="node-input-days-container"><i class="fa fa-calendar-o"></i> Days</label>
292
+ <span id="node-input-days-container"></span>
293
+ </div>
294
+
295
+ <div class="form-row">
296
+ <label for="node-input-validFrom"><i class="fa fa-calendar"></i> Valid from date</label>
297
+ <input type="text" id="node-input-validFrom" placeholder="YYYY-MM-DD" />
298
+ </div>
299
+
300
+ <div class="form-row">
301
+ <label for="node-input-validTo"><i class="fa fa-calendar"></i> Valid to date</label>
302
+ <input type="text" id="node-input-validTo" placeholder="YYYY-MM-DD" />
303
+ </div>
304
+ <h3>Output</h3>
305
+ <div class="form-row">
306
+ <div class="form-row">
307
+ <label for="node-input-outputValueForOn">Output value for on</label>
308
+ <input type="text" id="node-input-outputValueForOn" style="text-align: left; width: 120px">
309
+ <input type="hidden" id="node-input-outputValueForOntype">
310
+ </div>
311
+ <div class="form-row">
312
+ <label for="node-input-outputValueForOff">Output value for off</label>
313
+ <input type="text" id="node-input-outputValueForOff" style="text-align: left; width: 120px">
314
+ <input type="hidden" id="node-input-outputValueForOfftype">
315
+ </div>
316
+ <label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
317
+ <input type="checkbox"
318
+ id="node-input-sendCurrentValueWhenRescheduling"
319
+ style="display:inline-block; width:22px; vertical-align:top;"
320
+ autocomplete="off"><span>Send when rescheduling</span>
321
+ </label>
322
+ </div>
323
+ <div class="form-row">
324
+ <label for="node-input-outputIfNoSchedule">If no schedule, send</label>
325
+ <input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
326
+ </label>
327
+ </div>
328
+ <h3>Context storage</h3>
329
+ <div class="form-row">
330
+ <label for="node-input-contextStorage"><i class="fa fa-archive"></i> Context storage</label>
331
+ <input type="text" id="node-input-contextStorage" style="width: 160px">
332
+ </div>
333
+ </script>
334
+
335
+ <script type="text/markdown" data-help-name="ps-strategy-fixed-schedule">
336
+ A node you can use to save money by turning off and on a switch based on power prices.
337
+
338
+ Please read more in the [node documentation](https://powersaver.no/nodes/ps-strategy-fixed-schedule)
339
+ </script>