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.
- package/.eslintrc.js +15 -0
- package/docs/.vuepress/components/DonateButtons.vue +26 -3
- package/docs/.vuepress/components/VippsPlakat.vue +20 -0
- package/docs/.vuepress/config.js +17 -10
- package/docs/.vuepress/public/ads.txt +1 -0
- package/docs/README.md +4 -4
- package/docs/changelog/README.md +55 -1
- package/docs/contribute/README.md +8 -3
- package/docs/examples/example-grid-tariff-capacity-flow.json +23 -7
- package/docs/examples/example-grid-tariff-capacity-part.md +657 -22
- package/docs/faq/README.md +1 -1
- package/docs/faq/best-save-viewer.md +1 -1
- package/docs/guide/README.md +20 -5
- package/docs/images/best-save-config.png +0 -0
- package/docs/images/combine-two-lowest-price.png +0 -0
- package/docs/images/fixed-schedule-config.png +0 -0
- package/docs/images/global-context-window.png +0 -0
- package/docs/images/lowest-price-config.png +0 -0
- package/docs/images/node-ps-schedule-merger.png +0 -0
- package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
- package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
- package/docs/images/schedule-merger-config.png +0 -0
- package/docs/images/schedule-merger-example-1.png +0 -0
- package/docs/images/vipps-plakat.png +0 -0
- package/docs/images/vipps-qr.png +0 -0
- package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
- package/docs/nodes/README.md +12 -6
- package/docs/nodes/dynamic-commands.md +79 -0
- package/docs/nodes/dynamic-config.md +76 -0
- package/docs/nodes/ps-elvia-add-tariff.md +4 -0
- package/docs/nodes/ps-general-add-tariff.md +10 -0
- package/docs/nodes/ps-receive-price.md +2 -1
- package/docs/nodes/ps-schedule-merger.md +227 -0
- package/docs/nodes/ps-strategy-best-save.md +46 -110
- package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
- package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
- package/docs/nodes/ps-strategy-lowest-price.md +51 -112
- package/package.json +5 -2
- package/src/elvia/elvia-add-tariff.html +1 -2
- package/src/elvia/elvia-add-tariff.js +0 -1
- package/src/elvia/elvia-api.js +6 -0
- package/src/elvia/elvia-tariff.html +1 -1
- package/src/general-add-tariff.html +14 -8
- package/src/general-add-tariff.js +0 -1
- package/src/handle-input.js +94 -106
- package/src/handle-output.js +109 -0
- package/src/receive-price-functions.js +3 -3
- package/src/schedule-merger-functions.js +98 -0
- package/src/schedule-merger.html +135 -0
- package/src/schedule-merger.js +108 -0
- package/src/strategy-best-save.html +38 -1
- package/src/strategy-best-save.js +17 -63
- package/src/strategy-fixed-schedule.html +339 -0
- package/src/strategy-fixed-schedule.js +84 -0
- package/src/strategy-functions.js +35 -0
- package/src/strategy-lowest-price.html +76 -38
- package/src/strategy-lowest-price.js +16 -35
- package/src/utils.js +75 -2
- package/test/commands-input-best-save.test.js +142 -0
- package/test/commands-input-lowest-price.test.js +149 -0
- package/test/commands-input-schedule-merger.test.js +128 -0
- package/test/data/best-save-overlap-result.json +5 -1
- package/test/data/best-save-result.json +4 -0
- package/test/data/commands-result-best-save.json +383 -0
- package/test/data/commands-result-lowest-price.json +340 -0
- package/test/data/fixed-schedule-result.json +353 -0
- package/test/data/lowest-price-result-cont-max-fail.json +5 -1
- package/test/data/lowest-price-result-cont-max.json +3 -1
- package/test/data/lowest-price-result-cont.json +8 -1
- package/test/data/lowest-price-result-missing-end.json +8 -3
- package/test/data/lowest-price-result-neg-cont.json +27 -0
- package/test/data/lowest-price-result-neg-split.json +23 -0
- package/test/data/lowest-price-result-split-allday.json +3 -1
- package/test/data/lowest-price-result-split-allday10.json +1 -0
- package/test/data/lowest-price-result-split-max.json +3 -1
- package/test/data/lowest-price-result-split.json +3 -1
- package/test/data/merge-schedule-data.js +238 -0
- package/test/data/negative-prices.json +197 -0
- package/test/data/nordpool-event-prices.json +96 -480
- package/test/data/nordpool-zero-prices.json +90 -0
- package/test/data/reconfigResult.js +1 -0
- package/test/data/result.js +1 -0
- package/test/data/tibber-result-end-0-24h.json +12 -2
- package/test/data/tibber-result-end-0.json +12 -2
- package/test/data/tibber-result.json +1 -0
- package/test/receive-price.test.js +22 -0
- package/test/schedule-merger-functions.test.js +101 -0
- package/test/schedule-merger-test-utils.js +27 -0
- package/test/schedule-merger.test.js +130 -0
- package/test/send-config-input.test.js +45 -2
- package/test/strategy-best-save-test-utils.js +1 -1
- package/test/strategy-best-save.test.js +45 -0
- package/test/strategy-fixed-schedule.test.js +117 -0
- package/test/strategy-heat-capacitor.test.js +1 -1
- package/test/strategy-lowest-price-functions.test.js +1 -1
- package/test/strategy-lowest-price-test-utils.js +31 -0
- package/test/strategy-lowest-price.test.js +55 -45
- package/test/test-utils.js +43 -36
- package/test/utils.test.js +13 -0
- package/docs/images/node-power-saver.png +0 -0
- package/docs/nodes/power-saver.md +0 -23
- package/src/power-saver.html +0 -116
- package/src/power-saver.js +0 -260
- package/test/commands-input.test.js +0 -47
- package/test/power-saver.test.js +0 -189
|
@@ -0,0 +1,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 {
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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>
|