node-red-contrib-power-saver 2.1.0 → 3.0.3
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/.github/FUNDING.yml +12 -0
- package/CHANGELOG.md +1 -43
- package/README.md +3 -231
- package/docs/.vuepress/config.js +67 -0
- package/docs/.vuepress/dist/404.html +15 -0
- package/docs/.vuepress/dist/assets/css/styles.e835bef6.css +8 -0
- package/docs/.vuepress/dist/assets/img/back-to-top.8b37f773.svg +1 -0
- package/docs/.vuepress/dist/assets/img/elvia-config-no-config.b4bb972c.png +0 -0
- package/docs/.vuepress/dist/assets/img/elvia-config-no-tariff.3f89aba8.png +0 -0
- package/docs/.vuepress/dist/assets/img/elvia-config-select-tariff.0f73fd56.png +0 -0
- package/docs/.vuepress/dist/assets/img/elvia-config-subscription-key.8be8ab8a.png +0 -0
- package/docs/.vuepress/dist/assets/img/elvia-flow.bae2a4d5.png +0 -0
- package/docs/.vuepress/dist/assets/img/example-flow-1.3ff3e23f.png +0 -0
- package/docs/.vuepress/dist/assets/img/example-flow-2.b653b58d.png +0 -0
- package/docs/.vuepress/dist/assets/img/migrate-best-save.f73420f6.png +0 -0
- package/docs/.vuepress/dist/assets/img/migrate-power-saver.aae13f9d.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-power-saver.51ff2e5d.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-elvia-add-tariff.94ea2b09.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-receive-price.76eaa418.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-strategy-best-save.392292d5.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-strategy-lowest-price.3a4ad347.png +0 -0
- package/docs/.vuepress/dist/assets/img/power-saver-nordpool-current-state.bf14afde.png +0 -0
- package/docs/.vuepress/dist/assets/img/power-saver-nordpool-events-state.8c392507.png +0 -0
- package/docs/.vuepress/dist/assets/img/power-saver-tibber-mqtt.16891dd2.png +0 -0
- package/docs/.vuepress/dist/assets/js/293.5e967839.js +1 -0
- package/docs/.vuepress/dist/assets/js/491.c183eba3.js +1 -0
- package/docs/.vuepress/dist/assets/js/812.79dad458.js +2 -0
- package/docs/.vuepress/dist/assets/js/812.79dad458.js.LICENSE.txt +8 -0
- package/docs/.vuepress/dist/assets/js/app.4ee3384b.js +1 -0
- package/docs/.vuepress/dist/assets/js/runtime~app.cafd6537.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-08683c60.07fe8291.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0aca7ba6.aec5ba75.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.d008d8bc.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1ad821fa.85407071.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-30acb564.73b8e29f.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-3706649a.d7f73384.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-4637f9e4.22ab9413.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-510ed0d4.204a09ec.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-5954bcb2.be07962c.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-5db8da3a.ac192f35.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-61f728ca.802ab15e.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-677dfaed.9bbbd037.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-7c87f26e.457a1a60.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-8daa1a0e.db8b59c6.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-b4a42144.6e0c5aa0.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-e8c55052.5f85b6cd.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-fffb8e28.e815e852.js +1 -0
- package/docs/.vuepress/dist/changelog/index.html +15 -0
- package/docs/.vuepress/dist/contribute/index.html +15 -0
- package/docs/.vuepress/dist/euro.png +0 -0
- package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +169 -0
- package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +173 -0
- package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +182 -0
- package/docs/.vuepress/dist/examples/index.html +15 -0
- package/docs/.vuepress/dist/guide/index.html +52 -0
- package/docs/.vuepress/dist/index.html +15 -0
- package/docs/.vuepress/dist/logo.png +0 -0
- package/docs/.vuepress/dist/nodes/index.html +15 -0
- package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +97 -0
- package/docs/.vuepress/dist/nodes/power-saver.html +15 -0
- package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +15 -0
- package/docs/.vuepress/dist/nodes/ps-receive-price.html +80 -0
- package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +65 -0
- package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +89 -0
- package/docs/.vuepress/dist/nodes/strategy-input.html +40 -0
- package/docs/.vuepress/public/euro.png +0 -0
- package/docs/.vuepress/public/logo.png +0 -0
- package/docs/README.md +32 -0
- package/docs/changelog/README.md +71 -0
- package/docs/contribute/README.md +39 -0
- package/docs/examples/README.md +5 -0
- package/docs/examples/example-nordpool-current-state.md +166 -0
- package/docs/examples/example-nordpool-events-state.md +170 -0
- package/docs/examples/example-tibber-mqtt.md +179 -0
- package/docs/guide/README.md +202 -0
- package/docs/images/all-nodes.png +0 -0
- package/docs/images/best-save-config.png +0 -0
- package/docs/images/elvia-add-tariff-node-used.png +0 -0
- package/docs/images/elvia-config-no-config.png +0 -0
- package/docs/images/elvia-config-no-tariff.png +0 -0
- package/docs/images/elvia-config-select-tariff.png +0 -0
- package/docs/images/elvia-config-subscription-key.png +0 -0
- package/docs/images/elvia-flow.png +0 -0
- package/docs/images/elvia-tariff-config.png +0 -0
- package/docs/images/euro.png +0 -0
- package/docs/images/example-flow-1.png +0 -0
- package/docs/images/example-flow-2.png +0 -0
- package/docs/images/logo.png +0 -0
- package/docs/images/lowest-price-config.png +0 -0
- package/docs/images/migrate-best-save.png +0 -0
- package/docs/images/migrate-power-saver.png +0 -0
- package/docs/images/node-power-saver.png +0 -0
- package/docs/images/node-ps-elvia-add-tariff.png +0 -0
- package/docs/images/node-ps-elvia-tariff-types.png +0 -0
- package/docs/images/node-ps-elvia-tariff.png +0 -0
- package/docs/images/node-ps-receive-price.png +0 -0
- package/docs/images/node-ps-strategy-best-save.png +0 -0
- package/docs/images/node-ps-strategy-lowest-price.png +0 -0
- package/{doc → docs/images}/node-red-contrib-power-saver-flow.png +0 -0
- package/docs/images/power-saver-nordpool-current-state.png +0 -0
- package/docs/images/power-saver-nordpool-events-state.png +0 -0
- package/docs/images/power-saver-tibber-mqtt.png +0 -0
- package/docs/nodes/README.md +53 -0
- package/docs/nodes/old-power-saver-doc.md +231 -0
- package/docs/nodes/power-saver.md +23 -0
- package/docs/nodes/ps-elvia-add-tariff.md +52 -0
- package/docs/nodes/ps-receive-price.md +153 -0
- package/docs/nodes/ps-strategy-best-save.md +142 -0
- package/docs/nodes/ps-strategy-lowest-price.md +165 -0
- package/docs/nodes/strategy-input.md +39 -0
- package/package.json +16 -4
- package/src/elvia/elvia-add-tariff.html +70 -0
- package/src/elvia/elvia-add-tariff.js +47 -0
- package/src/elvia/elvia-api.js +61 -0
- package/src/elvia/elvia-config.html +46 -0
- package/src/elvia/elvia-config.js +19 -0
- package/src/elvia/elvia-tariff-types.html +34 -0
- package/src/elvia/elvia-tariff-types.js +25 -0
- package/src/elvia/elvia-tariff.html +89 -0
- package/src/elvia/elvia-tariff.js +22 -0
- package/src/elvia/icons/elvia_hvite.svg +4 -0
- package/src/elvia/icons/elvia_positive_4 copy.svg +4 -0
- package/src/handle-input.js +162 -0
- package/src/power-saver.html +116 -0
- package/{power-saver.js → src/power-saver.js} +9 -32
- package/src/receive-price-functions.js +99 -0
- package/src/receive-price.html +30 -0
- package/src/receive-price.js +21 -0
- package/src/strategy-best-save-functions.js +110 -0
- package/src/strategy-best-save.html +116 -0
- package/src/strategy-best-save.js +95 -0
- package/src/strategy-lowest-price-functions.js +35 -0
- package/src/strategy-lowest-price.html +168 -0
- package/src/strategy-lowest-price.js +125 -0
- package/{utils.js → src/utils.js} +44 -100
- package/test/data/adjustedResult.js +219 -71
- package/test/data/adjustedResult_old.js +154 -0
- package/test/data/best-save-result.json +357 -0
- package/test/data/converted-prices.json +197 -0
- package/test/data/elvia-input-grid-tariff.json +760 -0
- package/test/data/elvia-input-power-prices.json +194 -0
- package/test/data/elvia-output-add-tariff.json +290 -0
- package/test/data/lowest-price-result-cont.json +18 -0
- package/test/data/lowest-price-result-split-allday.json +21 -0
- package/test/data/lowest-price-result-split-allday10.json +20 -0
- package/test/data/lowest-price-result-split.json +20 -0
- package/test/data/nordpool-current-state-prices.json +283 -0
- package/test/data/nordpool-event-prices.json +574 -0
- package/test/data/reconfigResult.js +220 -67
- package/test/data/reconfigResult_old.js +141 -0
- package/test/data/tibber-data-end-0-24h.json +197 -0
- package/test/data/tibber-data-end-0.json +101 -0
- package/test/data/tibber-prices-single-home.json +64 -0
- package/test/data/tibber-prices.json +124 -0
- package/test/data/tibber-result-end-0-24h.json +320 -0
- package/test/data/tibber-result-end-0.json +168 -0
- package/test/data/{tibber_result.json → tibber-result.json} +0 -0
- package/test/elvia.test.js +26 -0
- package/test/mostSavedStrategy.test.js +22 -55
- package/test/power-saver.test.js +21 -5
- package/test/receive-price-functions.test.js +153 -0
- package/test/receive-price.test.js +122 -0
- package/test/send-config-input.test.js +8 -10
- package/test/strategy-best-save-test-utils.js +32 -0
- package/test/strategy-best-save.test.js +103 -0
- package/test/strategy-lowest-price-functions.test.js +40 -0
- package/test/strategy-lowest-price.test.js +538 -0
- package/test/test-utils.js +0 -18
- package/test/utils.test.js +22 -180
- package/doc/example-nordpool-current-state.md +0 -166
- package/doc/example-nordpool-events-state.md +0 -153
- package/doc/example-tibber-mqtt.md +0 -189
- package/doc/power-saver-nordpool-current-state.png +0 -0
- package/doc/power-saver-nordpool-events-state.png +0 -0
- package/doc/power-saver-tibber-mqtt.png +0 -0
- package/mostSavedStrategy.js +0 -84
- package/power-saver.html +0 -308
- package/test/data/tibber_data.json +0 -412
- package/test/data/tibber_prices.json +0 -412
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fetch = require("node-fetch");
|
|
2
|
+
|
|
3
|
+
function ping(node, subscriptionKey, setResultStatus = true) {
|
|
4
|
+
const url = "https://elvia.azure-api.net/grid-tariff/Ping";
|
|
5
|
+
const headers = { "Ocp-Apim-Subscription-Key": subscriptionKey };
|
|
6
|
+
fetch(url, { headers }).then((res) => {
|
|
7
|
+
if (setResultStatus) {
|
|
8
|
+
setNodeStatus(node, res.status);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getTariff(node, subscriptionKey, tariffKey, range = "today", setResultStatus = true) {
|
|
14
|
+
const url = "https://elvia.azure-api.net/grid-tariff/api/1/tariffquery?TariffKey=" + tariffKey + "&Range=" + range;
|
|
15
|
+
return get(node, subscriptionKey, url, setResultStatus);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getTariffForPeriod(node, subscriptionKey, tariffKey, startTime, endTime, setResultStatus = true) {
|
|
19
|
+
const url =
|
|
20
|
+
"https://elvia.azure-api.net/grid-tariff/api/1/tariffquery?TariffKey=" +
|
|
21
|
+
tariffKey +
|
|
22
|
+
"&StartTime=" +
|
|
23
|
+
startTime +
|
|
24
|
+
"&EndTime=" +
|
|
25
|
+
endTime;
|
|
26
|
+
return get(node, subscriptionKey, url, setResultStatus);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getTariffTypes(node, subscriptionKey, setResultStatus = true) {
|
|
30
|
+
const url = "https://elvia.azure-api.net/grid-tariff/api/1/tarifftype";
|
|
31
|
+
return get(node, subscriptionKey, url, setResultStatus);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function get(node, subscriptionKey, url, setResultStatus) {
|
|
35
|
+
const headers = { "Ocp-Apim-Subscription-Key": subscriptionKey };
|
|
36
|
+
return fetch(url, { headers }).then((res) => {
|
|
37
|
+
if (setResultStatus && node) {
|
|
38
|
+
setNodeStatus(node, res.status);
|
|
39
|
+
}
|
|
40
|
+
return res.json().then((json) => {
|
|
41
|
+
return json;
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setNodeStatus(node, status) {
|
|
47
|
+
if (status === 200) {
|
|
48
|
+
node.status({ fill: "green", shape: "dot", text: "Connected" });
|
|
49
|
+
} else if (status === 401) {
|
|
50
|
+
node.status({ fill: "red", shape: "dot", text: "Unauthorized" });
|
|
51
|
+
} else if (status === 403) {
|
|
52
|
+
node.status({ fill: "red", shape: "dot", text: "Forbidden" });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
ping,
|
|
58
|
+
getTariff,
|
|
59
|
+
getTariffForPeriod,
|
|
60
|
+
getTariffTypes,
|
|
61
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("ps-elvia-config", {
|
|
3
|
+
category: "config",
|
|
4
|
+
color: "#a6bbcf",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "Elvia Config" },
|
|
7
|
+
},
|
|
8
|
+
credentials: {
|
|
9
|
+
elviaSubscriptionKey: { type: "text" },
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
|
+
icon: "elvia_hvite.svg",
|
|
14
|
+
color: "#FFCC66",
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.name || "ps-elvia-tariff-types";
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script type="text/html" data-template-name="ps-elvia-config">
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Elvia config name</label>
|
|
24
|
+
<input type="text" id="node-config-input-name" style="width: 240px" />
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-config-input-elviaSubscriptionKey"><i class="fa fa-tag"></i> Subscription key</label>
|
|
28
|
+
<input
|
|
29
|
+
type="text"
|
|
30
|
+
id="node-config-input-elviaSubscriptionKey"
|
|
31
|
+
placeholder="your API subscription key from Elvia"
|
|
32
|
+
style="width: 240px"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script type="text/markdown" data-help-name="ps-elvia-tariff-types">
|
|
38
|
+
# Elvia Configuation
|
|
39
|
+
|
|
40
|
+
You must get your own Elvia API subscription key.
|
|
41
|
+
|
|
42
|
+
Go to the [Elvia Developer Portal](https://elvia.portal.azure-api.net/) to sign up,
|
|
43
|
+
and then request for a subscription to the GridTariffAPI.
|
|
44
|
+
When your subscription is approved, you will find your subscription key in the
|
|
45
|
+
[developer portal](https://elvia.portal.azure-api.net/developer) under Your subscriptions.
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { getTariffTypes } = require("./elvia-api");
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function ElviaConfigNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
|
|
7
|
+
this.elviaConfig = RED.nodes.getNode(config.elviaConfig);
|
|
8
|
+
|
|
9
|
+
// Store config in global configList
|
|
10
|
+
const configList = this.context().global.get("elviaConfigList") || [];
|
|
11
|
+
configList.push(config);
|
|
12
|
+
this.context().global.set("elviaConfigList", configList);
|
|
13
|
+
}
|
|
14
|
+
RED.nodes.registerType("ps-elvia-config", ElviaConfigNode, {
|
|
15
|
+
credentials: {
|
|
16
|
+
elviaSubscriptionKey: { type: "text" },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("ps-elvia-tariff-types", {
|
|
3
|
+
category: "Power Saver",
|
|
4
|
+
color: "#a6bbcf",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "Elvia Tariff Types" },
|
|
7
|
+
elviaConfig: { value: "", type: "ps-elvia-config" },
|
|
8
|
+
},
|
|
9
|
+
inputs: 1,
|
|
10
|
+
outputs: 1,
|
|
11
|
+
icon: "elvia_hvite.svg",
|
|
12
|
+
color: "#FFCC66",
|
|
13
|
+
label: function () {
|
|
14
|
+
return this.name || "Elvia Tariff Types";
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script type="text/html" data-template-name="ps-elvia-tariff-types">
|
|
20
|
+
<div class="form-row">
|
|
21
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
22
|
+
<input type="text" id="node-input-name" placeholder="Name" style="width: 240px" />
|
|
23
|
+
</div>
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-elviaConfig"><i class="fa fa-tag"></i> Elvia config</label>
|
|
26
|
+
<input type="text" id="node-input-elviaConfig" placeholder="Elvia config" style="width: 240px" />
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/markdown" data-help-name="ps-elvia-tariff-types">
|
|
31
|
+
# Elvia Tariff Types
|
|
32
|
+
|
|
33
|
+
A node to get the tariff types from Elvia. Send output to a debug node to see the result, or use it as you like.
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { getTariffTypes, ping } = require("./elvia-api");
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function PsElviaTariffTypesNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
this.elviaConfig = RED.nodes.getNode(config.elviaConfig);
|
|
7
|
+
const key = this.elviaConfig.credentials.elviaSubscriptionKey;
|
|
8
|
+
const node = this;
|
|
9
|
+
ping(node, key);
|
|
10
|
+
|
|
11
|
+
node.on("input", function () {
|
|
12
|
+
getTariffTypes(node, key).then((json) => {
|
|
13
|
+
node.send([{ payload: json }]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
RED.httpAdmin.get("/elvia-tariff-types", RED.auth.needsPermission("ps-elvia-config.read"), function (req, res) {
|
|
18
|
+
getTariffTypes(null, key).then((json) => {
|
|
19
|
+
res.json(json);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RED.nodes.registerType("ps-elvia-tariff-types", PsElviaTariffTypesNode);
|
|
25
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("ps-elvia-tariff", {
|
|
3
|
+
category: "Power Saver",
|
|
4
|
+
color: "#a6bbcf",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "Elvia Tariff" },
|
|
7
|
+
elviaConfig: { value: "", type: "ps-elvia-config" },
|
|
8
|
+
tariffKey: { value: "", required: true },
|
|
9
|
+
range: { value: "today", required: true },
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
|
+
// icon: "font-awesome/fa-power-off",
|
|
14
|
+
icon: "elvia_hvite.svg",
|
|
15
|
+
color: "#FFCC66",
|
|
16
|
+
label: function () {
|
|
17
|
+
return this.name || "Elvia Tariff";
|
|
18
|
+
},
|
|
19
|
+
oneditprepare: function () {
|
|
20
|
+
const readTariffTypes = function () {
|
|
21
|
+
const configId = $("#node-input-elviaConfig").val();
|
|
22
|
+
if (!configId) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
$.getJSON("elvia-tariff-types?configId=" + configId, function (data) {
|
|
26
|
+
if (!data.tariffTypes) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
$("#node-input-tariffKey").typedInput({
|
|
30
|
+
types: [
|
|
31
|
+
{
|
|
32
|
+
value: "tariffkeys",
|
|
33
|
+
options: data.tariffTypes.map((k) => {
|
|
34
|
+
return { value: k.tariffKey, label: k.title };
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
$("#node-input-range").typedInput({
|
|
43
|
+
types: [
|
|
44
|
+
{
|
|
45
|
+
value: "range",
|
|
46
|
+
options: [
|
|
47
|
+
{ value: "today", label: "today" },
|
|
48
|
+
{ value: "yesterday", label: "yesterday" },
|
|
49
|
+
{ value: "tomorrow", label: "tomorrow" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
$(".refresh-button").on("click", function () {
|
|
55
|
+
readTariffTypes();
|
|
56
|
+
});
|
|
57
|
+
$("#node-input-elviaConfig").on("change", function () {
|
|
58
|
+
readTariffTypes();
|
|
59
|
+
});
|
|
60
|
+
readTariffTypes();
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<script type="text/html" data-template-name="ps-elvia-tariff">
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
68
|
+
<input type="text" id="node-input-name" placeholder="Name" style="width: 300px" />
|
|
69
|
+
</div>
|
|
70
|
+
<div class="form-row">
|
|
71
|
+
<label for="node-input-elviaConfig"><i class="fa fa-tag"></i> Elvia config</label>
|
|
72
|
+
<input type="text" id="node-input-elviaConfig" placeholder="Elvia config" style="width: 300px" />
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-input-tariffKey"><i class="fa fa-tag"></i> Tariff key</label>
|
|
76
|
+
<input type="text" id="node-input-tariffKey" placeholder="Tariff" style="width: 300px" />
|
|
77
|
+
</div>
|
|
78
|
+
<div class="form-row">
|
|
79
|
+
<label for="node-input-range">Range</label>
|
|
80
|
+
<input type="text" id="node-input-range" style="width: 120px">
|
|
81
|
+
</label>
|
|
82
|
+
</div>
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<script type="text/markdown" data-help-name="ps-elvia-tariff">
|
|
86
|
+
# Elvia Tariff
|
|
87
|
+
|
|
88
|
+
A node to get the tariff from Elvia.
|
|
89
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const fetch = require("node-fetch");
|
|
2
|
+
const { getTariff, ping } = require("./elvia-api");
|
|
3
|
+
|
|
4
|
+
module.exports = function (RED) {
|
|
5
|
+
function PsElviaTariffNode(config) {
|
|
6
|
+
RED.nodes.createNode(this, config);
|
|
7
|
+
this.elviaConfig = RED.nodes.getNode(config.elviaConfig);
|
|
8
|
+
const key = this.elviaConfig.credentials.elviaSubscriptionKey;
|
|
9
|
+
this.tariffKey = config.tariffKey;
|
|
10
|
+
this.range = config.range;
|
|
11
|
+
const node = this;
|
|
12
|
+
ping(node, key);
|
|
13
|
+
|
|
14
|
+
node.on("input", function () {
|
|
15
|
+
getTariff(node, key, node.tariffKey, node.range).then((json) => {
|
|
16
|
+
node.send([{ payload: json }]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
RED.nodes.registerType("ps-elvia-tariff", PsElviaTariffNode);
|
|
22
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="72" height="72" viewBox="-14 -14 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M71.4075 30.4575H28.578V40.7831H71.4075V30.4575Z" fill="white" />
|
|
3
|
+
<path d="M36.379 60.915C22.5073 60.915 11.2459 49.5813 11.2459 35.6203C11.2459 21.6593 22.5073 10.3256 36.379 10.3256C44.3621 10.3256 51.4649 14.0832 56.0788 19.918L63.7887 13.1056C57.2929 5.10171 47.4279 0 36.379 0C16.8311 0 0.986298 15.9467 0.986298 35.6203C0.986298 55.294 16.8311 71.2406 36.379 71.2406C47.4279 71.2406 57.2929 66.1389 63.7887 58.135L56.0788 51.3226C51.4953 57.1575 44.3621 60.915 36.379 60.915Z" fill="white" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M71.4075 30.4575H28.578V40.7831H71.4075V30.4575Z" fill="black"/>
|
|
3
|
+
<path d="M36.379 60.915C22.5073 60.915 11.2459 49.5813 11.2459 35.6203C11.2459 21.6593 22.5073 10.3256 36.379 10.3256C44.3621 10.3256 51.4649 14.0832 56.0788 19.918L63.7887 13.1056C57.2929 5.10171 47.4279 0 36.379 0C16.8311 0 0.986298 15.9467 0.986298 35.6203C0.986298 55.294 16.8311 71.2406 36.379 71.2406C47.4279 71.2406 57.2929 66.1389 63.7887 58.135L56.0788 51.3226C51.4953 57.1575 44.3621 60.915 36.379 60.915Z" fill="black"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { getEffectiveConfig } = require("./utils");
|
|
2
|
+
const { extractPlanForDate } = require("./utils");
|
|
3
|
+
const { DateTime } = require("luxon");
|
|
4
|
+
|
|
5
|
+
function handleStrategyInput(node, msg, doPlanning) {
|
|
6
|
+
node.schedulingTimeout = null;
|
|
7
|
+
|
|
8
|
+
const effectiveConfig = getEffectiveConfig(node, msg);
|
|
9
|
+
if (!validateInput(node, msg)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const priceData = getPriceData(node, msg);
|
|
13
|
+
const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
|
|
14
|
+
|
|
15
|
+
// Store config variables in node
|
|
16
|
+
Object.keys(effectiveConfig).forEach((key) => (node[key] = effectiveConfig[key]));
|
|
17
|
+
|
|
18
|
+
clearTimeout(node.schedulingTimeout);
|
|
19
|
+
|
|
20
|
+
const dates = [...new Set(priceData.map((v) => DateTime.fromISO(v.start).toISODate()))];
|
|
21
|
+
|
|
22
|
+
// Load data from day before
|
|
23
|
+
const dateToday = DateTime.fromISO(dates[0]);
|
|
24
|
+
const dateDayBefore = DateTime.fromISO(dates[0]).plus({ days: -1 });
|
|
25
|
+
|
|
26
|
+
// Make plan
|
|
27
|
+
const plan = doPlanning(node, effectiveConfig, priceData, planFromTime, dateDayBefore, dateToday);
|
|
28
|
+
|
|
29
|
+
// Save schedule
|
|
30
|
+
node.context().set("lastPlan", plan);
|
|
31
|
+
dates.forEach((d) => saveDayData(node, d, extractPlanForDate(plan, d)));
|
|
32
|
+
|
|
33
|
+
// Prepare output
|
|
34
|
+
let output1 = null;
|
|
35
|
+
let output2 = null;
|
|
36
|
+
let output3 = {
|
|
37
|
+
payload: {
|
|
38
|
+
schedule: plan.schedule,
|
|
39
|
+
hours: plan.hours,
|
|
40
|
+
source: msg.payload.source,
|
|
41
|
+
config: effectiveConfig,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Find current output, and set output (if configured to do)
|
|
46
|
+
const pastSchedule = plan.schedule.filter((entry) => DateTime.fromISO(entry.time) <= planFromTime);
|
|
47
|
+
|
|
48
|
+
const sendNow = node.sendCurrentValueWhenRescheduling && pastSchedule.length > 0;
|
|
49
|
+
if (sendNow) {
|
|
50
|
+
const currentValue = pastSchedule[pastSchedule.length - 1].value;
|
|
51
|
+
output1 = currentValue ? { payload: true } : null;
|
|
52
|
+
output2 = currentValue ? null : { payload: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Delete old data
|
|
56
|
+
deleteSavedScheduleBefore(node, dateDayBefore);
|
|
57
|
+
|
|
58
|
+
// Send output
|
|
59
|
+
node.send([output1, output2, output3]);
|
|
60
|
+
|
|
61
|
+
// Run schedule
|
|
62
|
+
node.schedulingTimeout = runSchedule(node, plan.schedule, planFromTime, sendNow);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getPriceData(node, msg) {
|
|
66
|
+
const isConfigMsg = !!msg?.payload?.config;
|
|
67
|
+
if (isConfigMsg) {
|
|
68
|
+
return node.context().get("lastPriceData");
|
|
69
|
+
}
|
|
70
|
+
const priceData = msg.payload.priceData;
|
|
71
|
+
node.context().set("lastPriceData", priceData);
|
|
72
|
+
return priceData;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runSchedule(node, schedule, time, currentSent = false) {
|
|
76
|
+
let currentTime = time;
|
|
77
|
+
let remainingSchedule = schedule.filter((entry) => {
|
|
78
|
+
return DateTime.fromISO(entry.time) > DateTime.fromISO(time);
|
|
79
|
+
});
|
|
80
|
+
if (remainingSchedule.length > 0) {
|
|
81
|
+
const entry = remainingSchedule[0];
|
|
82
|
+
const nextTime = DateTime.fromISO(entry.time);
|
|
83
|
+
const wait = nextTime - currentTime;
|
|
84
|
+
const onOff = entry.value ? "on" : "off";
|
|
85
|
+
node.log("Switching " + onOff + " in " + wait + " milliseconds");
|
|
86
|
+
const statusMessage = `Scheduled ${remainingSchedule.length} changes. Next: ${
|
|
87
|
+
remainingSchedule[0].value ? "on" : "off"
|
|
88
|
+
}`;
|
|
89
|
+
node.status({ fill: "green", shape: "dot", text: statusMessage });
|
|
90
|
+
return setTimeout(() => {
|
|
91
|
+
sendSwitch(node, entry.value);
|
|
92
|
+
node.schedulingTimeout = runSchedule(node, remainingSchedule, nextTime);
|
|
93
|
+
}, wait);
|
|
94
|
+
} else {
|
|
95
|
+
const message = "No schedule";
|
|
96
|
+
node.warn(message);
|
|
97
|
+
node.status({ fill: "red", shape: "dot", text: message });
|
|
98
|
+
if (!currentSent) {
|
|
99
|
+
sendSwitch(node, node.outputIfNoSchedule);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function deleteSavedScheduleBefore(node, day) {
|
|
105
|
+
let date = day;
|
|
106
|
+
do {
|
|
107
|
+
date = date.plus({ days: -1 });
|
|
108
|
+
data = node.context().get(date.toISO());
|
|
109
|
+
} while (data);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveDayData(node, date, plan) {
|
|
113
|
+
node.context().set(date, plan);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sendSwitch(node, onOff) {
|
|
117
|
+
const output1 = onOff ? { payload: true } : null;
|
|
118
|
+
const output2 = onOff ? null : { payload: false };
|
|
119
|
+
node.send([output1, output2, null]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateInput(node, msg) {
|
|
123
|
+
if (!msg.payload) {
|
|
124
|
+
validationFailure(node, "No payload");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (typeof msg.payload !== "object") {
|
|
128
|
+
validationFailure(node, "Payload is not an object");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (msg.payload.config !== undefined) {
|
|
132
|
+
return true; // Got config msg
|
|
133
|
+
}
|
|
134
|
+
if (msg.payload.priceData === undefined) {
|
|
135
|
+
validationFailure(node, "Payload is missing priceData");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (msg.payload.priceData.length === undefined) {
|
|
139
|
+
validationFailure(node, "Illegal priceData in payload. Did you use the receive-price node?", "Illegal payload");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (msg.payload.priceData.length === 0) {
|
|
143
|
+
validationFailure(node, "priceData is empty");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
msg.payload.priceData.forEach((h) => {
|
|
147
|
+
if (!h.start || !h.value) {
|
|
148
|
+
validationFailure(node, "Malformed entries in priceData. All entries must contain start and value.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validationFailure(node, message, status = null) {
|
|
156
|
+
node.status({ fill: "red", shape: "ring", text: status ?? message });
|
|
157
|
+
node.warn(message);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
handleStrategyInput,
|
|
162
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("power-saver", {
|
|
3
|
+
category: "Power Saver",
|
|
4
|
+
color: "#a6bbcf",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "Power Saver" },
|
|
7
|
+
maxHoursToSaveInSequence: {
|
|
8
|
+
value: 3,
|
|
9
|
+
required: true,
|
|
10
|
+
validate: RED.validators.number(),
|
|
11
|
+
},
|
|
12
|
+
minHoursOnAfterMaxSequenceSaved: {
|
|
13
|
+
value: 2,
|
|
14
|
+
required: true,
|
|
15
|
+
validate: RED.validators.number(),
|
|
16
|
+
},
|
|
17
|
+
minSaving: {
|
|
18
|
+
value: 0.01,
|
|
19
|
+
required: true,
|
|
20
|
+
validate: RED.validators.number(),
|
|
21
|
+
},
|
|
22
|
+
sendCurrentValueWhenRescheduling: {
|
|
23
|
+
value: true,
|
|
24
|
+
required: true,
|
|
25
|
+
// validate: RED.validators.number(),
|
|
26
|
+
align: "left",
|
|
27
|
+
},
|
|
28
|
+
outputIfNoSchedule: { value: "true", required: true, align: "left" },
|
|
29
|
+
scheduleOnlyFromCurrentTime: {
|
|
30
|
+
value: "true",
|
|
31
|
+
required: true,
|
|
32
|
+
align: "left",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
inputs: 1,
|
|
36
|
+
outputs: 3,
|
|
37
|
+
icon: "font-awesome/fa-eur",
|
|
38
|
+
color: "#FFCC66",
|
|
39
|
+
label: function () {
|
|
40
|
+
return this.name || "Power Saver";
|
|
41
|
+
},
|
|
42
|
+
outputLabels: ["on", "off", "schedule"],
|
|
43
|
+
oneditprepare: function () {
|
|
44
|
+
$("#node-input-outputIfNoSchedule").typedInput({
|
|
45
|
+
types: [
|
|
46
|
+
{
|
|
47
|
+
value: "onoff",
|
|
48
|
+
options: [
|
|
49
|
+
{ value: "true", label: "On" },
|
|
50
|
+
{ value: "false", label: "Off" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
$("#node-input-scheduleOnlyFromCurrentTime").typedInput({
|
|
56
|
+
types: [
|
|
57
|
+
{
|
|
58
|
+
value: "nowOrStart",
|
|
59
|
+
options: [
|
|
60
|
+
{ value: "false", label: "Whole data set" },
|
|
61
|
+
{ value: "true", label: "From current hour" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<script type="text/html" data-template-name="power-saver">
|
|
71
|
+
<div class="form-row">
|
|
72
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
73
|
+
<input type="text" id="node-input-name" placeholder="Name" style="width: 240px">
|
|
74
|
+
</div>
|
|
75
|
+
<h3>Rules</h3>
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-maxHoursToSaveInSequence"><i class="fa fa-arrows-h"></i> Max per sequence</label>
|
|
78
|
+
<input type="text" id="node-input-maxHoursToSaveInSequence" style="width: 80px" placeholder="Max hours to save in sequence">
|
|
79
|
+
</div>
|
|
80
|
+
<div class="form-row">
|
|
81
|
+
<label for="node-input-minHoursOnAfterMaxSequenceSaved"><i class="fa fa-ellipsis-h"></i> Min recover</label>
|
|
82
|
+
<input type="text"
|
|
83
|
+
id="node-input-minHoursOnAfterMaxSequenceSaved"
|
|
84
|
+
style="width: 80px"
|
|
85
|
+
placeholder="Min hours on after a max sequence">
|
|
86
|
+
</div>
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-input-minSaving"><i class="fa fa-eur"></i> Min saving</label>
|
|
89
|
+
<input type="text" id="node-input-minSaving" placeholder="Minimum to save for turning off" style="width: 80px">
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-input-scheduleOnlyFromCurrentTime">Schedule for</label>
|
|
93
|
+
<input type="text" id="node-input-scheduleOnlyFromCurrentTime" style="width: 160px">
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
<h3>Output</h3>
|
|
97
|
+
<div class="form-row">
|
|
98
|
+
<label for="node-input-sendCurrentValueWhenRescheduling" style="width:240px">
|
|
99
|
+
<input type="checkbox"
|
|
100
|
+
id="node-input-sendCurrentValueWhenRescheduling"
|
|
101
|
+
style="display:inline-block; width:22px; vertical-align:top;"
|
|
102
|
+
autocomplete="off"><span>Send when rescheduling</span>
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="form-row">
|
|
106
|
+
<label for="node-input-outputIfNoSchedule">If no schedule, send</label>
|
|
107
|
+
<input type="text" id="node-input-outputIfNoSchedule" style="width: 80px">
|
|
108
|
+
</label>
|
|
109
|
+
</div>
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<script type="text/markdown" data-help-name="power-saver">
|
|
113
|
+
A node you can use to save money by turning off and on a switch based on power prices.
|
|
114
|
+
|
|
115
|
+
Please read more in the [node documentation](https://ottopaulsen.github.io/node-red-contrib-power-saver/nodes/power-saver)
|
|
116
|
+
</script>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
const { DateTime } = require("luxon");
|
|
2
2
|
const {
|
|
3
|
-
convertMsg,
|
|
4
3
|
countAtEnd,
|
|
5
|
-
makeSchedule,
|
|
6
|
-
getSavings,
|
|
7
4
|
extractPlanForDate,
|
|
8
|
-
getStartAtIndex,
|
|
9
5
|
getDiff,
|
|
6
|
+
getEffectiveConfig,
|
|
7
|
+
getSavings,
|
|
8
|
+
getStartAtIndex,
|
|
9
|
+
loadDayData,
|
|
10
|
+
makeSchedule,
|
|
10
11
|
} = require("./utils");
|
|
11
|
-
const
|
|
12
|
+
const { convertMsg } = require("./receive-price-functions");
|
|
13
|
+
const { calculate } = require("./strategy-best-save-functions");
|
|
12
14
|
|
|
13
15
|
let schedulingTimeout = null;
|
|
14
16
|
|
|
@@ -37,7 +39,7 @@ module.exports = function (RED) {
|
|
|
37
39
|
if (!priceData) {
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
|
-
const planFromTime = msg.payload.time
|
|
42
|
+
const planFromTime = msg.payload.time ? DateTime.fromISO(msg.payload.time) : DateTime.now();
|
|
41
43
|
|
|
42
44
|
// Store config variables in node
|
|
43
45
|
Object.keys(effectiveConfig).forEach((key) => (node[key] = effectiveConfig[key]));
|
|
@@ -116,19 +118,6 @@ function adjustSavingsPassedHours(plan, includeFromLastPlanHours) {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
function getEffectiveConfig(node, msg) {
|
|
120
|
-
const res = node.context().get("config");
|
|
121
|
-
const isConfigMsg = !!msg?.payload?.config;
|
|
122
|
-
if (isConfigMsg) {
|
|
123
|
-
const inputConfig = msg.payload.config;
|
|
124
|
-
Object.keys(inputConfig).forEach((key) => {
|
|
125
|
-
res[key] = inputConfig[key];
|
|
126
|
-
});
|
|
127
|
-
node.context().set("config", res);
|
|
128
|
-
}
|
|
129
|
-
return res;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
121
|
function getPriceData(node, msg) {
|
|
133
122
|
const isConfigMsg = !!msg?.payload?.config;
|
|
134
123
|
if (isConfigMsg) {
|
|
@@ -149,18 +138,6 @@ function getPriceData(node, msg) {
|
|
|
149
138
|
return priceData;
|
|
150
139
|
}
|
|
151
140
|
|
|
152
|
-
function loadDayData(node, date) {
|
|
153
|
-
// Load saved schedule for the date (YYYY-MM-DD)
|
|
154
|
-
// Return null if not found
|
|
155
|
-
const key = date.toISODate();
|
|
156
|
-
const saved = node.context().get(key);
|
|
157
|
-
const res = saved ?? {
|
|
158
|
-
schedule: [],
|
|
159
|
-
hours: [],
|
|
160
|
-
};
|
|
161
|
-
return res;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
141
|
function loadDataJustBefore(node, dateDayBefore, dateToday, startAtIndex) {
|
|
165
142
|
const dataDayBefore = loadDayData(node, dateDayBefore);
|
|
166
143
|
const dataToday = loadDayData(node, dateToday);
|
|
@@ -188,7 +165,7 @@ function makePlan(node, values, startTimes, onOffBefore, firstValueNextDay) {
|
|
|
188
165
|
const lastCountDayBefore = countAtEnd(onOffBefore, lastValueDayBefore);
|
|
189
166
|
const onOff =
|
|
190
167
|
strategy === "mostSaved"
|
|
191
|
-
?
|
|
168
|
+
? calculate(
|
|
192
169
|
values,
|
|
193
170
|
node.maxHoursToSaveInSequence,
|
|
194
171
|
node.minHoursOnAfterMaxSequenceSaved,
|