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.
Files changed (179) hide show
  1. package/.github/FUNDING.yml +12 -0
  2. package/CHANGELOG.md +1 -43
  3. package/README.md +3 -231
  4. package/docs/.vuepress/config.js +67 -0
  5. package/docs/.vuepress/dist/404.html +15 -0
  6. package/docs/.vuepress/dist/assets/css/styles.e835bef6.css +8 -0
  7. package/docs/.vuepress/dist/assets/img/back-to-top.8b37f773.svg +1 -0
  8. package/docs/.vuepress/dist/assets/img/elvia-config-no-config.b4bb972c.png +0 -0
  9. package/docs/.vuepress/dist/assets/img/elvia-config-no-tariff.3f89aba8.png +0 -0
  10. package/docs/.vuepress/dist/assets/img/elvia-config-select-tariff.0f73fd56.png +0 -0
  11. package/docs/.vuepress/dist/assets/img/elvia-config-subscription-key.8be8ab8a.png +0 -0
  12. package/docs/.vuepress/dist/assets/img/elvia-flow.bae2a4d5.png +0 -0
  13. package/docs/.vuepress/dist/assets/img/example-flow-1.3ff3e23f.png +0 -0
  14. package/docs/.vuepress/dist/assets/img/example-flow-2.b653b58d.png +0 -0
  15. package/docs/.vuepress/dist/assets/img/migrate-best-save.f73420f6.png +0 -0
  16. package/docs/.vuepress/dist/assets/img/migrate-power-saver.aae13f9d.png +0 -0
  17. package/docs/.vuepress/dist/assets/img/node-power-saver.51ff2e5d.png +0 -0
  18. package/docs/.vuepress/dist/assets/img/node-ps-elvia-add-tariff.94ea2b09.png +0 -0
  19. package/docs/.vuepress/dist/assets/img/node-ps-receive-price.76eaa418.png +0 -0
  20. package/docs/.vuepress/dist/assets/img/node-ps-strategy-best-save.392292d5.png +0 -0
  21. package/docs/.vuepress/dist/assets/img/node-ps-strategy-lowest-price.3a4ad347.png +0 -0
  22. package/docs/.vuepress/dist/assets/img/power-saver-nordpool-current-state.bf14afde.png +0 -0
  23. package/docs/.vuepress/dist/assets/img/power-saver-nordpool-events-state.8c392507.png +0 -0
  24. package/docs/.vuepress/dist/assets/img/power-saver-tibber-mqtt.16891dd2.png +0 -0
  25. package/docs/.vuepress/dist/assets/js/293.5e967839.js +1 -0
  26. package/docs/.vuepress/dist/assets/js/491.c183eba3.js +1 -0
  27. package/docs/.vuepress/dist/assets/js/812.79dad458.js +2 -0
  28. package/docs/.vuepress/dist/assets/js/812.79dad458.js.LICENSE.txt +8 -0
  29. package/docs/.vuepress/dist/assets/js/app.4ee3384b.js +1 -0
  30. package/docs/.vuepress/dist/assets/js/runtime~app.cafd6537.js +1 -0
  31. package/docs/.vuepress/dist/assets/js/v-08683c60.07fe8291.js +1 -0
  32. package/docs/.vuepress/dist/assets/js/v-0aca7ba6.aec5ba75.js +1 -0
  33. package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.d008d8bc.js +1 -0
  34. package/docs/.vuepress/dist/assets/js/v-1ad821fa.85407071.js +1 -0
  35. package/docs/.vuepress/dist/assets/js/v-30acb564.73b8e29f.js +1 -0
  36. package/docs/.vuepress/dist/assets/js/v-3706649a.d7f73384.js +1 -0
  37. package/docs/.vuepress/dist/assets/js/v-4637f9e4.22ab9413.js +1 -0
  38. package/docs/.vuepress/dist/assets/js/v-510ed0d4.204a09ec.js +1 -0
  39. package/docs/.vuepress/dist/assets/js/v-5954bcb2.be07962c.js +1 -0
  40. package/docs/.vuepress/dist/assets/js/v-5db8da3a.ac192f35.js +1 -0
  41. package/docs/.vuepress/dist/assets/js/v-61f728ca.802ab15e.js +1 -0
  42. package/docs/.vuepress/dist/assets/js/v-677dfaed.9bbbd037.js +1 -0
  43. package/docs/.vuepress/dist/assets/js/v-7c87f26e.457a1a60.js +1 -0
  44. package/docs/.vuepress/dist/assets/js/v-8daa1a0e.db8b59c6.js +1 -0
  45. package/docs/.vuepress/dist/assets/js/v-b4a42144.6e0c5aa0.js +1 -0
  46. package/docs/.vuepress/dist/assets/js/v-e8c55052.5f85b6cd.js +1 -0
  47. package/docs/.vuepress/dist/assets/js/v-fffb8e28.e815e852.js +1 -0
  48. package/docs/.vuepress/dist/changelog/index.html +15 -0
  49. package/docs/.vuepress/dist/contribute/index.html +15 -0
  50. package/docs/.vuepress/dist/euro.png +0 -0
  51. package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +169 -0
  52. package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +173 -0
  53. package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +182 -0
  54. package/docs/.vuepress/dist/examples/index.html +15 -0
  55. package/docs/.vuepress/dist/guide/index.html +52 -0
  56. package/docs/.vuepress/dist/index.html +15 -0
  57. package/docs/.vuepress/dist/logo.png +0 -0
  58. package/docs/.vuepress/dist/nodes/index.html +15 -0
  59. package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +97 -0
  60. package/docs/.vuepress/dist/nodes/power-saver.html +15 -0
  61. package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +15 -0
  62. package/docs/.vuepress/dist/nodes/ps-receive-price.html +80 -0
  63. package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +65 -0
  64. package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +89 -0
  65. package/docs/.vuepress/dist/nodes/strategy-input.html +40 -0
  66. package/docs/.vuepress/public/euro.png +0 -0
  67. package/docs/.vuepress/public/logo.png +0 -0
  68. package/docs/README.md +32 -0
  69. package/docs/changelog/README.md +71 -0
  70. package/docs/contribute/README.md +39 -0
  71. package/docs/examples/README.md +5 -0
  72. package/docs/examples/example-nordpool-current-state.md +166 -0
  73. package/docs/examples/example-nordpool-events-state.md +170 -0
  74. package/docs/examples/example-tibber-mqtt.md +179 -0
  75. package/docs/guide/README.md +202 -0
  76. package/docs/images/all-nodes.png +0 -0
  77. package/docs/images/best-save-config.png +0 -0
  78. package/docs/images/elvia-add-tariff-node-used.png +0 -0
  79. package/docs/images/elvia-config-no-config.png +0 -0
  80. package/docs/images/elvia-config-no-tariff.png +0 -0
  81. package/docs/images/elvia-config-select-tariff.png +0 -0
  82. package/docs/images/elvia-config-subscription-key.png +0 -0
  83. package/docs/images/elvia-flow.png +0 -0
  84. package/docs/images/elvia-tariff-config.png +0 -0
  85. package/docs/images/euro.png +0 -0
  86. package/docs/images/example-flow-1.png +0 -0
  87. package/docs/images/example-flow-2.png +0 -0
  88. package/docs/images/logo.png +0 -0
  89. package/docs/images/lowest-price-config.png +0 -0
  90. package/docs/images/migrate-best-save.png +0 -0
  91. package/docs/images/migrate-power-saver.png +0 -0
  92. package/docs/images/node-power-saver.png +0 -0
  93. package/docs/images/node-ps-elvia-add-tariff.png +0 -0
  94. package/docs/images/node-ps-elvia-tariff-types.png +0 -0
  95. package/docs/images/node-ps-elvia-tariff.png +0 -0
  96. package/docs/images/node-ps-receive-price.png +0 -0
  97. package/docs/images/node-ps-strategy-best-save.png +0 -0
  98. package/docs/images/node-ps-strategy-lowest-price.png +0 -0
  99. package/{doc → docs/images}/node-red-contrib-power-saver-flow.png +0 -0
  100. package/docs/images/power-saver-nordpool-current-state.png +0 -0
  101. package/docs/images/power-saver-nordpool-events-state.png +0 -0
  102. package/docs/images/power-saver-tibber-mqtt.png +0 -0
  103. package/docs/nodes/README.md +53 -0
  104. package/docs/nodes/old-power-saver-doc.md +231 -0
  105. package/docs/nodes/power-saver.md +23 -0
  106. package/docs/nodes/ps-elvia-add-tariff.md +52 -0
  107. package/docs/nodes/ps-receive-price.md +153 -0
  108. package/docs/nodes/ps-strategy-best-save.md +142 -0
  109. package/docs/nodes/ps-strategy-lowest-price.md +165 -0
  110. package/docs/nodes/strategy-input.md +39 -0
  111. package/package.json +16 -4
  112. package/src/elvia/elvia-add-tariff.html +70 -0
  113. package/src/elvia/elvia-add-tariff.js +47 -0
  114. package/src/elvia/elvia-api.js +61 -0
  115. package/src/elvia/elvia-config.html +46 -0
  116. package/src/elvia/elvia-config.js +19 -0
  117. package/src/elvia/elvia-tariff-types.html +34 -0
  118. package/src/elvia/elvia-tariff-types.js +25 -0
  119. package/src/elvia/elvia-tariff.html +89 -0
  120. package/src/elvia/elvia-tariff.js +22 -0
  121. package/src/elvia/icons/elvia_hvite.svg +4 -0
  122. package/src/elvia/icons/elvia_positive_4 copy.svg +4 -0
  123. package/src/handle-input.js +162 -0
  124. package/src/power-saver.html +116 -0
  125. package/{power-saver.js → src/power-saver.js} +9 -32
  126. package/src/receive-price-functions.js +99 -0
  127. package/src/receive-price.html +30 -0
  128. package/src/receive-price.js +21 -0
  129. package/src/strategy-best-save-functions.js +110 -0
  130. package/src/strategy-best-save.html +116 -0
  131. package/src/strategy-best-save.js +95 -0
  132. package/src/strategy-lowest-price-functions.js +35 -0
  133. package/src/strategy-lowest-price.html +168 -0
  134. package/src/strategy-lowest-price.js +125 -0
  135. package/{utils.js → src/utils.js} +44 -100
  136. package/test/data/adjustedResult.js +219 -71
  137. package/test/data/adjustedResult_old.js +154 -0
  138. package/test/data/best-save-result.json +357 -0
  139. package/test/data/converted-prices.json +197 -0
  140. package/test/data/elvia-input-grid-tariff.json +760 -0
  141. package/test/data/elvia-input-power-prices.json +194 -0
  142. package/test/data/elvia-output-add-tariff.json +290 -0
  143. package/test/data/lowest-price-result-cont.json +18 -0
  144. package/test/data/lowest-price-result-split-allday.json +21 -0
  145. package/test/data/lowest-price-result-split-allday10.json +20 -0
  146. package/test/data/lowest-price-result-split.json +20 -0
  147. package/test/data/nordpool-current-state-prices.json +283 -0
  148. package/test/data/nordpool-event-prices.json +574 -0
  149. package/test/data/reconfigResult.js +220 -67
  150. package/test/data/reconfigResult_old.js +141 -0
  151. package/test/data/tibber-data-end-0-24h.json +197 -0
  152. package/test/data/tibber-data-end-0.json +101 -0
  153. package/test/data/tibber-prices-single-home.json +64 -0
  154. package/test/data/tibber-prices.json +124 -0
  155. package/test/data/tibber-result-end-0-24h.json +320 -0
  156. package/test/data/tibber-result-end-0.json +168 -0
  157. package/test/data/{tibber_result.json → tibber-result.json} +0 -0
  158. package/test/elvia.test.js +26 -0
  159. package/test/mostSavedStrategy.test.js +22 -55
  160. package/test/power-saver.test.js +21 -5
  161. package/test/receive-price-functions.test.js +153 -0
  162. package/test/receive-price.test.js +122 -0
  163. package/test/send-config-input.test.js +8 -10
  164. package/test/strategy-best-save-test-utils.js +32 -0
  165. package/test/strategy-best-save.test.js +103 -0
  166. package/test/strategy-lowest-price-functions.test.js +40 -0
  167. package/test/strategy-lowest-price.test.js +538 -0
  168. package/test/test-utils.js +0 -18
  169. package/test/utils.test.js +22 -180
  170. package/doc/example-nordpool-current-state.md +0 -166
  171. package/doc/example-nordpool-events-state.md +0 -153
  172. package/doc/example-tibber-mqtt.md +0 -189
  173. package/doc/power-saver-nordpool-current-state.png +0 -0
  174. package/doc/power-saver-nordpool-events-state.png +0 -0
  175. package/doc/power-saver-tibber-mqtt.png +0 -0
  176. package/mostSavedStrategy.js +0 -84
  177. package/power-saver.html +0 -308
  178. package/test/data/tibber_data.json +0 -412
  179. 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 mostSavedStrategy = require("./mostSavedStrategy");
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 ?? DateTime.now();
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
- ? mostSavedStrategy.calculate(
168
+ ? calculate(
192
169
  values,
193
170
  node.maxHoursToSaveInSequence,
194
171
  node.minHoursOnAfterMaxSequenceSaved,