node-red-contrib-power-saver 3.3.2 → 3.4.2

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 (137) hide show
  1. package/.firebase/hosting.ZG9jcy8udnVlcHJlc3MvZGlzdA.cache +94 -0
  2. package/.firebaserc +5 -0
  3. package/.github/workflows/firebase-hosting-merge.yml +20 -0
  4. package/.github/workflows/firebase-hosting-pull-request.yml +17 -0
  5. package/README.md +1 -1
  6. package/docs/.vuepress/components/BestSaveVerificator.vue +3 -3
  7. package/docs/.vuepress/config.js +15 -1
  8. package/docs/.vuepress/dist/404.html +23 -5
  9. package/docs/.vuepress/dist/assets/css/896.styles.21a80cb6.css +1 -0
  10. package/docs/.vuepress/dist/assets/css/styles.1c48cbd0.css +10 -0
  11. package/docs/.vuepress/dist/assets/img/heat-capacitor-temperatureVsPrice.6e74905b.png +0 -0
  12. package/docs/.vuepress/dist/assets/img/node-ps-strategy-heat-capacitor-cascade-control.2e75ed9e.png +0 -0
  13. package/docs/.vuepress/dist/assets/img/node-ps-strategy-heat-capacitor-simple-flow-example.29d9bf59.png +0 -0
  14. package/docs/.vuepress/dist/assets/img/oven-setpoint-calculation.5bda0eec.png +0 -0
  15. package/docs/.vuepress/dist/assets/img/overshoot-time.b3b5d70e.png +0 -0
  16. package/docs/.vuepress/dist/assets/js/229.5c5378fa.js +1 -0
  17. package/docs/.vuepress/dist/assets/js/331.872104cd.js +1 -0
  18. package/docs/.vuepress/dist/assets/js/405.f4edd94d.js +2 -0
  19. package/docs/.vuepress/dist/assets/js/{619.8ba1b1f6.js.LICENSE.txt → 405.f4edd94d.js.LICENSE.txt} +0 -0
  20. package/docs/.vuepress/dist/assets/js/490.1e639e05.js +1 -0
  21. package/docs/.vuepress/dist/assets/js/{491.17a98f38.js → 491.bd938119.js} +1 -1
  22. package/docs/.vuepress/dist/assets/js/555.d8963d84.js +1 -0
  23. package/docs/.vuepress/dist/assets/js/{811.6a3392d5.js → 811.5f659592.js} +0 -0
  24. package/docs/.vuepress/dist/assets/js/app.dfdee6f9.js +1 -0
  25. package/docs/.vuepress/dist/assets/js/runtime~app.f6ac32d7.js +1 -0
  26. package/docs/.vuepress/dist/assets/js/v-0607240a.0193a377.js +1 -0
  27. package/docs/.vuepress/dist/assets/js/v-08683c60.52e94cb6.js +1 -0
  28. package/docs/.vuepress/dist/assets/js/v-0aca7ba6.cac5d4b9.js +1 -0
  29. package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.18561f6e.js +1 -0
  30. package/docs/.vuepress/dist/assets/js/v-1ad821fa.6697a349.js +1 -0
  31. package/docs/.vuepress/dist/assets/js/v-1b3a0ab8.c6c4e19b.js +1 -0
  32. package/docs/.vuepress/dist/assets/js/v-1e2b191e.07b8ab21.js +1 -0
  33. package/docs/.vuepress/dist/assets/js/v-29504124.00be7399.js +1 -0
  34. package/docs/.vuepress/dist/assets/js/v-30acb564.28af12af.js +1 -0
  35. package/docs/.vuepress/dist/assets/js/{v-3706649a.d7f73384.js → v-3706649a.c76d575b.js} +1 -1
  36. package/docs/.vuepress/dist/assets/js/v-4637f9e4.d334c29a.js +1 -0
  37. package/docs/.vuepress/dist/assets/js/v-4c28314d.8cbb0f9d.js +1 -0
  38. package/docs/.vuepress/dist/assets/js/v-510ed0d4.c04bc2e4.js +1 -0
  39. package/docs/.vuepress/dist/assets/js/{v-5954bcb2.937005d0.js → v-5954bcb2.dff3fc67.js} +1 -1
  40. package/docs/.vuepress/dist/assets/js/v-5db8da3a.e5e6d7a6.js +1 -0
  41. package/docs/.vuepress/dist/assets/js/v-61f728ca.81968036.js +1 -0
  42. package/docs/.vuepress/dist/assets/js/v-677dfaed.c159b0f4.js +1 -0
  43. package/docs/.vuepress/dist/assets/js/v-7446a652.8fc2c591.js +1 -0
  44. package/docs/.vuepress/dist/assets/js/v-7c87f26e.8ed52391.js +1 -0
  45. package/docs/.vuepress/dist/assets/js/v-84304104.f3f07ed3.js +1 -0
  46. package/docs/.vuepress/dist/assets/js/{v-8daa1a0e.c63afc2b.js → v-8daa1a0e.ed84ca09.js} +1 -1
  47. package/docs/.vuepress/dist/assets/js/{v-b4a42144.733e4e7c.js → v-b4a42144.9a2a0c9f.js} +1 -1
  48. package/docs/.vuepress/dist/assets/js/v-e8c55052.b7d52fc6.js +1 -0
  49. package/docs/.vuepress/dist/assets/js/v-fffb8e28.d09ab959.js +1 -0
  50. package/docs/.vuepress/dist/changelog/index.html +23 -5
  51. package/docs/.vuepress/dist/contribute/index.html +23 -5
  52. package/docs/.vuepress/dist/examples/example-cascade-temperature-control.html +304 -0
  53. package/docs/.vuepress/dist/examples/example-heat-capacitor.html +247 -0
  54. package/docs/.vuepress/dist/examples/example-next-schedule-entity.html +27 -9
  55. package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +24 -6
  56. package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +24 -6
  57. package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +24 -6
  58. package/docs/.vuepress/dist/examples/index.html +23 -5
  59. package/docs/.vuepress/dist/faq/best-save-viewer.html +23 -5
  60. package/docs/.vuepress/dist/faq/index.html +23 -5
  61. package/docs/.vuepress/dist/guide/index.html +24 -6
  62. package/docs/.vuepress/dist/index.html +23 -5
  63. package/docs/.vuepress/dist/nodes/index.html +23 -5
  64. package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +25 -7
  65. package/docs/.vuepress/dist/nodes/power-saver.html +23 -5
  66. package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +23 -5
  67. package/docs/.vuepress/dist/nodes/ps-general-add-tariff.html +23 -5
  68. package/docs/.vuepress/dist/nodes/ps-receive-price.html +26 -8
  69. package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +32 -9
  70. package/docs/.vuepress/dist/nodes/ps-strategy-heat-capacitor.html +260 -0
  71. package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +30 -7
  72. package/docs/.vuepress/dist/nodes/strategy-input.html +24 -6
  73. package/docs/README.md +1 -1
  74. package/docs/changelog/README.md +14 -0
  75. package/docs/contribute/README.md +8 -0
  76. package/docs/examples/README.md +8 -2
  77. package/docs/examples/example-cascade-temperature-control.md +346 -0
  78. package/docs/examples/example-heat-capacitor.md +271 -0
  79. package/docs/images/heat-capacitor-temperatureVsPrice.png +0 -0
  80. package/docs/images/node-ps-strategy-heat-capacitor-cascade-control.png +0 -0
  81. package/docs/images/node-ps-strategy-heat-capacitor-simple-flow-example.png +0 -0
  82. package/docs/images/node-ps-strategy-heat-capacitor.png +0 -0
  83. package/docs/images/oven-setpoint-calculation.png +0 -0
  84. package/docs/images/overshoot-time.png +0 -0
  85. package/docs/images/temperature-manipulation-config.png +0 -0
  86. package/docs/nodes/README.md +7 -1
  87. package/docs/nodes/ps-strategy-heat-capacitor.md +346 -0
  88. package/examples/add-general-tariff.json +103 -0
  89. package/examples/best-save-for-water-heater.json +140 -0
  90. package/examples/elvia-add-tariff.json +99 -0
  91. package/examples/elvia-get-tariff-types.json +58 -0
  92. package/examples/elvia-get-tariff.json +60 -0
  93. package/examples/heat-capacitor-for-room-heating.json +186 -0
  94. package/examples/lowest-price-for-heating-cables.json +159 -0
  95. package/firebase.json +6 -0
  96. package/package.json +17 -9
  97. package/public/404.html +33 -0
  98. package/public/index.html +89 -0
  99. package/src/elvia/elvia-tariff.js +2 -1
  100. package/src/handle-input.js +6 -3
  101. package/src/strategy-heat-capacitor-functions.js +246 -0
  102. package/src/strategy-heat-capacitor.html +85 -0
  103. package/src/strategy-heat-capacitor.js +125 -0
  104. package/test/data/converted-prices.json +1 -1
  105. package/test/data/multiple-trades.json +53 -0
  106. package/test/data/tibber-decreasing-24h.json +101 -0
  107. package/test/data/tibber-decreasing2-24h.json +101 -0
  108. package/test/strategy-heat-capacitor-node.test.js +183 -0
  109. package/test/strategy-heat-capacitor.test.js +103 -0
  110. package/test/strategy-lowest-price-functions.test.js +1 -1
  111. package/test/utils.test.js +0 -2
  112. package/docs/.vuepress/dist/.nojekyll +0 -0
  113. package/docs/.vuepress/dist/assets/css/563.styles.99f4a8aa.css +0 -1
  114. package/docs/.vuepress/dist/assets/css/styles.031dcf27.css +0 -9
  115. package/docs/.vuepress/dist/assets/js/262.cf2c57d2.js +0 -1
  116. package/docs/.vuepress/dist/assets/js/293.08ea5200.js +0 -1
  117. package/docs/.vuepress/dist/assets/js/331.15ee3c51.js +0 -1
  118. package/docs/.vuepress/dist/assets/js/619.8ba1b1f6.js +0 -2
  119. package/docs/.vuepress/dist/assets/js/app.b705176c.js +0 -1
  120. package/docs/.vuepress/dist/assets/js/runtime~app.47f4f812.js +0 -1
  121. package/docs/.vuepress/dist/assets/js/v-0607240a.a57c2199.js +0 -1
  122. package/docs/.vuepress/dist/assets/js/v-08683c60.ccafdcab.js +0 -1
  123. package/docs/.vuepress/dist/assets/js/v-0aca7ba6.25903946.js +0 -1
  124. package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.a6a015b4.js +0 -1
  125. package/docs/.vuepress/dist/assets/js/v-1ad821fa.5978386f.js +0 -1
  126. package/docs/.vuepress/dist/assets/js/v-1e2b191e.88dc5555.js +0 -1
  127. package/docs/.vuepress/dist/assets/js/v-29504124.4aca27d5.js +0 -1
  128. package/docs/.vuepress/dist/assets/js/v-30acb564.529a3c16.js +0 -1
  129. package/docs/.vuepress/dist/assets/js/v-4637f9e4.703b1d96.js +0 -1
  130. package/docs/.vuepress/dist/assets/js/v-510ed0d4.7b142a81.js +0 -1
  131. package/docs/.vuepress/dist/assets/js/v-5db8da3a.3de3588d.js +0 -1
  132. package/docs/.vuepress/dist/assets/js/v-61f728ca.21d432fe.js +0 -1
  133. package/docs/.vuepress/dist/assets/js/v-677dfaed.44a653b9.js +0 -1
  134. package/docs/.vuepress/dist/assets/js/v-7446a652.74b21d0b.js +0 -1
  135. package/docs/.vuepress/dist/assets/js/v-7c87f26e.ee5be992.js +0 -1
  136. package/docs/.vuepress/dist/assets/js/v-e8c55052.ab0a79ec.js +0 -1
  137. package/docs/.vuepress/dist/assets/js/v-fffb8e28.525be02a.js +0 -1
@@ -0,0 +1,89 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Welcome to Firebase Hosting</title>
7
+
8
+ <!-- update the version number as needed -->
9
+ <script defer src="/__/firebase/9.6.6/firebase-app-compat.js"></script>
10
+ <!-- include only the Firebase features as you need -->
11
+ <script defer src="/__/firebase/9.6.6/firebase-auth-compat.js"></script>
12
+ <script defer src="/__/firebase/9.6.6/firebase-database-compat.js"></script>
13
+ <script defer src="/__/firebase/9.6.6/firebase-firestore-compat.js"></script>
14
+ <script defer src="/__/firebase/9.6.6/firebase-functions-compat.js"></script>
15
+ <script defer src="/__/firebase/9.6.6/firebase-messaging-compat.js"></script>
16
+ <script defer src="/__/firebase/9.6.6/firebase-storage-compat.js"></script>
17
+ <script defer src="/__/firebase/9.6.6/firebase-analytics-compat.js"></script>
18
+ <script defer src="/__/firebase/9.6.6/firebase-remote-config-compat.js"></script>
19
+ <script defer src="/__/firebase/9.6.6/firebase-performance-compat.js"></script>
20
+ <!--
21
+ initialize the SDK after all desired features are loaded, set useEmulator to false
22
+ to avoid connecting the SDK to running emulators.
23
+ -->
24
+ <script defer src="/__/firebase/init.js?useEmulator=true"></script>
25
+
26
+ <style media="screen">
27
+ body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
28
+ #message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px; border-radius: 3px; }
29
+ #message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
30
+ #message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
31
+ #message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
32
+ #message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
33
+ #message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
34
+ #load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
35
+ @media (max-width: 600px) {
36
+ body, #message { margin-top: 0; background: white; box-shadow: none; }
37
+ body { border-top: 16px solid #ffa100; }
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div id="message">
43
+ <h2>Welcome</h2>
44
+ <h1>Firebase Hosting Setup Complete</h1>
45
+ <p>You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!</p>
46
+ <a target="_blank" href="https://firebase.google.com/docs/hosting/">Open Hosting Documentation</a>
47
+ </div>
48
+ <p id="load">Firebase SDK Loading&hellip;</p>
49
+
50
+ <script>
51
+ document.addEventListener('DOMContentLoaded', function() {
52
+ const loadEl = document.querySelector('#load');
53
+ // // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
54
+ // // The Firebase SDK is initialized and available here!
55
+ //
56
+ // firebase.auth().onAuthStateChanged(user => { });
57
+ // firebase.database().ref('/path/to/ref').on('value', snapshot => { });
58
+ // firebase.firestore().doc('/foo/bar').get().then(() => { });
59
+ // firebase.functions().httpsCallable('yourFunction')().then(() => { });
60
+ // firebase.messaging().requestPermission().then(() => { });
61
+ // firebase.storage().ref('/path/to/ref').getDownloadURL().then(() => { });
62
+ // firebase.analytics(); // call to activate
63
+ // firebase.analytics().logEvent('tutorial_completed');
64
+ // firebase.performance(); // call to activate
65
+ //
66
+ // // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
67
+
68
+ try {
69
+ let app = firebase.app();
70
+ let features = [
71
+ 'auth',
72
+ 'database',
73
+ 'firestore',
74
+ 'functions',
75
+ 'messaging',
76
+ 'storage',
77
+ 'analytics',
78
+ 'remoteConfig',
79
+ 'performance',
80
+ ].filter(feature => typeof app[feature] === 'function');
81
+ loadEl.textContent = `Firebase SDK loaded with ${features.join(', ')}`;
82
+ } catch (e) {
83
+ console.error(e);
84
+ loadEl.textContent = 'Error loading the Firebase SDK, check the console.';
85
+ }
86
+ });
87
+ </script>
88
+ </body>
89
+ </html>
@@ -1,5 +1,6 @@
1
1
  const fetch = require("node-fetch");
2
- const { getTariff, ping } = require("./elvia-api");
2
+
3
+ const { getTariff, ping } = require("./elvia-api.js");
3
4
 
4
5
  module.exports = function (RED) {
5
6
  function PsElviaTariffNode(config) {
@@ -12,7 +12,7 @@ function handleStrategyInput(node, msg, doPlanning) {
12
12
  node.warn("Resetting node context by command");
13
13
  // Reset all saved data
14
14
  node.context().set(["lastPlan", "lastPriceData", "lastSource"], [undefined, undefined, undefined]);
15
- deleteSavedScheduleBefore(node, DateTime.now().plus({ days: 1 }), 100);
15
+ deleteSavedScheduleBefore(node, DateTime.now().plus({ days: 2 }), 100);
16
16
  }
17
17
  const { priceData, source } = getPriceData(node, msg);
18
18
  if (!priceData) {
@@ -125,12 +125,14 @@ function runSchedule(node, schedule, time, currentSent = false) {
125
125
 
126
126
  function deleteSavedScheduleBefore(node, day, checkDays = 0) {
127
127
  let date = day;
128
+ let data = null;
128
129
  let count = 0;
129
130
  do {
130
131
  date = date.plus({ days: -1 });
131
- data = node.context().set(date.toISO(), undefined);
132
+ data = node.context().get(date.toISODate());
133
+ node.context().set(date.toISODate(), undefined);
132
134
  count++;
133
- } while (data || count <= checkDays);
135
+ } while (data !== undefined || count <= checkDays);
134
136
  }
135
137
 
136
138
  function saveDayData(node, date, plan) {
@@ -181,4 +183,5 @@ function validateInput(node, msg) {
181
183
 
182
184
  module.exports = {
183
185
  handleStrategyInput,
186
+ validateInput,
184
187
  };
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ const { DateTime } = require("luxon");
3
+ const { roundPrice } = require("./utils");
4
+
5
+ function calculateOpportunities(prices, pattern, amount) {
6
+ //creating a price vector with minute granularity
7
+ const tempPrice = Array(prices.length * 60).fill(0);
8
+ for (let i = 0; i < prices.length; i++) {
9
+ tempPrice.fill(prices[i], i * 60, (i + 1) * 60);
10
+ //debugger;
11
+ }
12
+
13
+ //Calculate weighted pattern
14
+ const weight = amount / pattern.reduce((a, b) => a + b, 0); //last calculates the sum of all numbers in the pattern
15
+ const weightedPattern = pattern.map((x) => x * weight);
16
+
17
+ //Calculating procurement opportunities. Sliding the pattern over the price vector to find the price for procuring
18
+ //at time t
19
+ const dot = (a, b) => a.map((x, i) => a[i] * b[i]).reduce((m, n) => m + n);
20
+ const procurementOpportunities = Array(prices.length * 60 - pattern.length + 1);
21
+ for (let i = 0; i < procurementOpportunities.length; i++) {
22
+ procurementOpportunities[i] = dot(weightedPattern, tempPrice.slice(i, i + pattern.length));
23
+ }
24
+ return procurementOpportunities;
25
+ }
26
+
27
+ // This function finds the buy sell
28
+ // schedule for maximum profit
29
+ // two vectors containing the buy and sell indexes are returned in an array
30
+ function findBestBuySellPattern(priceBuy, buyLength, priceSell, sellLength) {
31
+ // Traverse through given price array
32
+ const buyIndexes = [];
33
+ const sellIndexes = [];
34
+ let i = 0;
35
+ while (i < priceBuy.length - 1) {
36
+ // Find Local Minima
37
+ // Note that the limit is (n-2) as we are
38
+ // comparing present element to the next element
39
+ while (i < priceBuy.length - 1 && priceBuy[i + 1] < priceBuy[i]) i++;
40
+
41
+ // If we reached the end, break
42
+ // as no further solution is possible
43
+ if (i == priceBuy.length - 1) break;
44
+
45
+ // Store the index of minima
46
+ buyIndexes.push(i);
47
+ // Move the next allowed maxima away from the minima - required due to the asymmetric buy/sell prices
48
+ i = i + Math.round(buyLength / 2);
49
+ // Find Local Maxima
50
+ // Note that the limit is (n-1) as we are
51
+ // comparing to previous element
52
+ while (i < priceSell.length && priceSell[i] >= priceSell[i - 1]) i++;
53
+
54
+ // Store the index of maxima
55
+ sellIndexes.push(i - 1);
56
+ i = i + Math.round(sellLength / 2);
57
+ }
58
+ return [buyIndexes, sellIndexes];
59
+ }
60
+
61
+ function calculateValueDictList(buySell, buyPrices, sellPrices, startDate) {
62
+ const buySellValueDictList = [];
63
+ for (let i = 0; i < buySell[0].length; i++) {
64
+ const buyDateTime = startDate.plus({ minutes: buySell[0][i] });
65
+ const sellDateTime = startDate.plus({ minutes: buySell[1][i] });
66
+ if (i != 0) {
67
+ const prevSellDateTime = startDate.plus({ minutes: buySell[1][i - 1] });
68
+ buySellValueDictList.push({
69
+ type: "sell - buy",
70
+ tradeValue: roundPrice(sellPrices[buySell[1][i - 1]] - buyPrices[buySell[0][i]]),
71
+ buyIndex: buySell[0][i],
72
+ buyDate: buyDateTime,
73
+ buyPrice: roundPrice(buyPrices[buySell[0][i]]),
74
+ sellIndex: buySell[1][i - 1],
75
+ sellDate: prevSellDateTime,
76
+ sellPrice: roundPrice(sellPrices[buySell[1][i - 1]]),
77
+ });
78
+ }
79
+ buySellValueDictList.push({
80
+ type: "buy - sell",
81
+ tradeValue: roundPrice(sellPrices[buySell[1][i]] - buyPrices[buySell[0][i]]),
82
+ buyIndex: buySell[0][i],
83
+ buyDate: buyDateTime,
84
+ buyPrice: roundPrice(buyPrices[buySell[0][i]]),
85
+ sellIndex: buySell[1][i],
86
+ sellDate: sellDateTime,
87
+ sellPrice: roundPrice(sellPrices[buySell[1][i]]),
88
+ });
89
+ }
90
+ return buySellValueDictList;
91
+ }
92
+
93
+ function removeLowBuySellPairs(buySellPattern, buyPrices, sellPrices, minSavings, startDate) {
94
+ let lowestSaving = -1;
95
+ const buySellClone = Array.from(buySellPattern);
96
+
97
+ while (minSavings >= lowestSaving) {
98
+ const dictList = calculateValueDictList(buySellClone, buyPrices, sellPrices, startDate);
99
+ if (dictList.length === 0) {
100
+ return buySellClone;
101
+ }
102
+ let sellIndex = 0;
103
+ let buyIndex = 0;
104
+ for (let i = 0; i < dictList.length; i++) {
105
+ if (i == 0 || dictList[i].tradeValue < lowestSaving) {
106
+ lowestSaving = dictList[i].tradeValue;
107
+ sellIndex = dictList[i].sellIndex;
108
+ buyIndex = dictList[i].buyIndex;
109
+ }
110
+ }
111
+ if (lowestSaving <= minSavings) {
112
+ buySellClone[0] = buySellClone[0].filter((x) => x != buyIndex);
113
+ buySellClone[1] = buySellClone[1].filter((x) => x != sellIndex);
114
+ }
115
+ }
116
+ return buySellClone;
117
+ }
118
+
119
+ function calculateSchedule(
120
+ startDate,
121
+ buySellStackedArray,
122
+ buyPrices,
123
+ sellPrices,
124
+ maxTempAdjustment,
125
+ boostTempHeat,
126
+ boostTempCool,
127
+ buyDuration,
128
+ sellDuration
129
+ ) {
130
+ const arrayLength = buyPrices.length;
131
+ const schedule = {
132
+ startAt: startDate,
133
+ temperatures: Array(arrayLength),
134
+ maxTempAdjustment: maxTempAdjustment,
135
+ durationInMinutes: arrayLength,
136
+ boostTempHeat: boostTempHeat,
137
+ boostTempCool: boostTempCool,
138
+ heatingDuration: buyDuration,
139
+ coolingDuration: sellDuration,
140
+ };
141
+
142
+ if (buySellStackedArray[0].length === 0) {
143
+ //No procurements or sales scheduled
144
+ schedule.temperatures.fill(-maxTempAdjustment, 0, arrayLength);
145
+ } else {
146
+ let lastBuyIndex = 0;
147
+ let boostHeat;
148
+ let boostCool;
149
+ for (let i = 0; i < buySellStackedArray[0].length; i++) {
150
+ const buyIndex = buySellStackedArray[1][i];
151
+ const sellIndex = buySellStackedArray[0][i];
152
+
153
+ //If this is the start of the time-series, do not boost the temperatures
154
+ sellIndex == 0 ? (boostHeat = 0) : (boostHeat = boostTempHeat);
155
+ lastBuyIndex == 0 ? (boostCool = 0) : (boostCool = boostTempCool);
156
+
157
+ //Cooling period. Adding boosted cooling temperature for the period of divestment
158
+ if (sellIndex - lastBuyIndex <= sellDuration) {
159
+ schedule.temperatures.fill(-maxTempAdjustment - boostCool, lastBuyIndex, sellIndex);
160
+ } else {
161
+ schedule.temperatures.fill(-maxTempAdjustment - boostCool, lastBuyIndex, lastBuyIndex + sellDuration);
162
+ schedule.temperatures.fill(-maxTempAdjustment, lastBuyIndex + sellDuration, sellIndex);
163
+ }
164
+ //Heating period. Adding boosted heating temperature for the period of procurement
165
+ if (buyIndex - sellIndex <= buyDuration) {
166
+ schedule.temperatures.fill(maxTempAdjustment + boostHeat, sellIndex, buyIndex);
167
+ } else {
168
+ schedule.temperatures.fill(maxTempAdjustment + boostHeat, sellIndex, sellIndex + buyDuration);
169
+ schedule.temperatures.fill(maxTempAdjustment, sellIndex + buyDuration, buyIndex);
170
+ }
171
+
172
+ lastBuyIndex = buyIndex;
173
+ }
174
+
175
+ //final fill
176
+ if (arrayLength - lastBuyIndex <= sellDuration) {
177
+ schedule.temperatures.fill(-maxTempAdjustment - boostCool, lastBuyIndex, arrayLength);
178
+ } else {
179
+ schedule.temperatures.fill(-maxTempAdjustment - boostCool, lastBuyIndex, lastBuyIndex + sellDuration);
180
+ schedule.temperatures.fill(-maxTempAdjustment, lastBuyIndex + sellDuration, arrayLength);
181
+ }
182
+ }
183
+
184
+ schedule.trades = calculateValueDictList(buySellStackedArray, buyPrices, sellPrices, startDate);
185
+ return schedule;
186
+ }
187
+
188
+ function findTemp(date, schedule) {
189
+ let diff = Math.round(date.diff(schedule.startAt).as("minutes"));
190
+ return schedule.temperatures[diff];
191
+ }
192
+
193
+ function runBuySellAlgorithm(
194
+ priceData,
195
+ timeHeat1C,
196
+ timeCool1C,
197
+ boostTempHeat,
198
+ boostTempCool,
199
+ maxTempAdjustment,
200
+ minSavings
201
+ ) {
202
+ const prices = [...priceData.map((pd) => pd.value)];
203
+ const startDate = DateTime.fromISO(priceData[0].start);
204
+
205
+ //pattern for how much power is procured/sold when.
206
+ //This has, for now, just a flat acquisition/divestment profile
207
+ const buyDuration = Math.round(timeHeat1C * maxTempAdjustment * 2);
208
+ const sellDuration = Math.round(timeCool1C * maxTempAdjustment * 2);
209
+ const buyPattern = Array(buyDuration).fill(1);
210
+ const sellPattern = Array(sellDuration).fill(1);
211
+
212
+ //Calculate what it will cost to procure/sell 1 kWh as a function of time
213
+ const buyPrices = calculateOpportunities(prices, buyPattern, 1);
214
+ const sellPrices = calculateOpportunities(prices, sellPattern, 1);
215
+
216
+ //Find dates for when to procure/sell
217
+ const buySell = findBestBuySellPattern(buyPrices, buyPattern.length, sellPrices, sellPattern.length);
218
+
219
+ //Remove small/disputable gains (least profitable buy/sell pairs)
220
+ const buySellCleaned = removeLowBuySellPairs(buySell, buyPrices, sellPrices, minSavings, startDate);
221
+
222
+ //Calculate temperature adjustment as a function of time
223
+ const schedule = calculateSchedule(
224
+ startDate,
225
+ buySellCleaned,
226
+ buyPrices,
227
+ sellPrices,
228
+ maxTempAdjustment,
229
+ boostTempHeat,
230
+ boostTempCool,
231
+ buyDuration,
232
+ sellDuration
233
+ );
234
+
235
+ return schedule;
236
+ }
237
+
238
+ module.exports = {
239
+ runBuySellAlgorithm,
240
+ findTemp,
241
+ calculateOpportunities,
242
+ findBestBuySellPattern,
243
+ calculateValueDictList,
244
+ removeLowBuySellPairs,
245
+ calculateSchedule,
246
+ };
@@ -0,0 +1,85 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("ps-strategy-heat-capacitor", {
3
+ category: "Power Saver",
4
+ color: "#FFCC66",
5
+ defaults: {
6
+ name: { value: "Heat capacitor" },
7
+ timeHeat1C: { value: 50, required: true, align: "left" },
8
+ timeCool1C: { value: 50, required: true, align: "left" },
9
+ maxTempAdjustment: { value: 0.5, required: true, align: "left" },
10
+ boostTempHeat: { value: 0, required: true, align: "left" },
11
+ boostTempCool: { value: 0, required: true, align: "left" },
12
+ minSavings: { value: 0.08, required: true, align: "left" },
13
+ setpoint: { value: 23, required: true, align: "left" },
14
+ },
15
+ inputs: 1,
16
+ outputs: 3,
17
+ color: "#FFCC66",
18
+ icon: "font-awesome/fa-bar-chart",
19
+ label: function () {
20
+ return this.name || "Heat capacitor";
21
+ },
22
+ outputLabels: ["T", "dT", "schedule"],
23
+ });
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="ps-strategy-heat-capacitor">
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" placeholder="Name" />
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-timeHeat1C"><i class="icon-tag"></i> Time +1C [minutes]</label>
33
+ <input type="text" id="node-input-timeHeat1C" />
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-timeCool1C"><i class="icon-tag"></i> Time -1C [minutes]</label>
37
+ <input type="text" id="node-input-timeCool1C" />
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-setpoint"><i class="icon-tag"></i> Setpoint</label>
41
+ <input type="text" id="node-input-setpoint" />
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-maxTempAdjustment"><i class="icon-tag"></i> Max temp adj.</label>
45
+ <input type="text" id="node-input-maxTempAdjustment" />
46
+ </div>
47
+ <div class="form-row">
48
+ <label for="node-input-minSavings"><i class="icon-tag"></i> Min Savings</label>
49
+ <input type="text" id="node-input-minSavings" />
50
+ </div>
51
+ <div class="form-row">
52
+ <label for="node-input-boostTempHeat"><i class="icon-tag"></i> Heating Boost [C]</label>
53
+ <input type="text" id="node-input-boostTempHeat" />
54
+ </div>
55
+ <div class="form-row">
56
+ <label for="node-input-boostTempCool"><i class="icon-tag"></i> Cooling Boost [C]</label>
57
+ <input type="text" id="node-input-boostTempCool" />
58
+ </div>
59
+ </script>
60
+
61
+ <script type="text/markdown" data-help-name="ps-strategy-heat-capacitor">
62
+ # ps-strategy-heat-capacitor
63
+
64
+ A strategy for moving consumption from expensive to cheap periods utilizing climate entities.
65
+
66
+ ## Description
67
+
68
+ The heat capacitor strategy utilizes a large body of mass, like your house or cabin, to procure heat at a time where electricity is cheap, and divest at a time where electricity is expensive. This is achieved by increasing the temperature setpoint of one or several climate entities at times when electricity is cheap, and reducing it when electricity is expensive.
69
+
70
+ It is a good application for cabins/heated storage spaces, as the entity never actually shuts off the climate entities and should therefore be rather safe to apply (still at you own risk :-)). It can also be used for you house, jacuzzi, and/or pool.
71
+
72
+ | Value | Description |
73
+ | ----------------- | -------------------------------------------------------------------------- |
74
+ | Time + 1C | The time required to increase the temperature by 1C. |
75
+ | Time - 1C | The time required to decrease the temperature by 1C. |
76
+ | Setpoint | Ideal temperature in C |
77
+ | Max temp adj. | The number of degrees the system is allowed to increase/decrease. |
78
+ | Heating Boost [C] | An extra increase in temperature to the setpoint for the investment period |
79
+ | Cooling Boost [C] | An extra decrease in temperature to the setpoint for the divestment period |
80
+ | Min Savings | The minimum amount of savings required for a buy/sell cycle. |
81
+
82
+
83
+ Please read more in the [node documentation](https://ottopaulsen.github.io/node-red-contrib-power-saver/nodes/ps-strategy-heat-capacitor)
84
+
85
+ </script>
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ const { DateTime } = require("luxon");
3
+ const { validateInput } = require("./handle-input");
4
+ const { runBuySellAlgorithm, findTemp } = require("./strategy-heat-capacitor-functions");
5
+ const { version } = require("../package.json");
6
+
7
+ module.exports = function (RED) {
8
+ function TempMan(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+
12
+ node.timeHeat1C = Number(config.timeHeat1C);
13
+ node.timeCool1C = Number(config.timeCool1C);
14
+ node.setpoint = Number(config.setpoint);
15
+ node.maxTempAdjustment = Number(config.maxTempAdjustment);
16
+ node.boostTempHeat = Number(config.boostTempHeat);
17
+ node.boostTempCool = Number(config.boostTempCool);
18
+ node.minSavings = Number(config.minSavings);
19
+ // sanitise disabled output as this is used when all else fails
20
+ if (isNaN(node.disabled_op)) {
21
+ node.disabled_op = 0;
22
+ }
23
+
24
+ node.on("close", function () {
25
+ clearTimeout(node.schedulingTimeout);
26
+ });
27
+
28
+ this.on("input", function (msg) {
29
+ if (validateInput(node, msg)) {
30
+ // Using msg.payload.config to change specific properties
31
+ if (msg.hasOwnProperty("payload")) {
32
+ if (msg.payload.hasOwnProperty("config")) {
33
+ if (msg.payload.config.hasOwnProperty("timeHeat1C"))
34
+ node.timeHeat1C = Number(msg.payload.config.timeHeat1C);
35
+ if (msg.payload.config.hasOwnProperty("timeCool1C"))
36
+ node.timeCool1C = Number(msg.payload.config.timeCool1C);
37
+ if (msg.payload.config.hasOwnProperty("setpoint")) node.setpoint = Number(msg.payload.config.setpoint);
38
+ if (msg.payload.config.hasOwnProperty("maxTempAdjustment"))
39
+ node.maxTempAdjustment = Number(msg.payload.config.maxTempAdjustment);
40
+ if (msg.payload.config.hasOwnProperty("boostTempHeat"))
41
+ node.boostTempHeat = Number(msg.payload.config.boostTempHeat);
42
+ if (msg.payload.config.hasOwnProperty("boostTempCool"))
43
+ node.boostTempCool = Number(msg.payload.config.boostTempCool);
44
+ if (msg.payload.config.hasOwnProperty("minSavings"))
45
+ node.minSavings = Number(msg.payload.config.minSavings);
46
+ }
47
+
48
+ //merge pricedata to escape some midnight issues. Store max 72 hour history
49
+ if (msg.payload.hasOwnProperty("priceData")) {
50
+ if (node.hasOwnProperty("priceData")) {
51
+ node.priceData = mergePriceData(node.priceData, msg.payload.priceData);
52
+ if (node.priceData.length > 72) node.priceData = node.priceData.slice(-72);
53
+ } else {
54
+ node.priceData = msg.payload.priceData;
55
+ }
56
+ }
57
+
58
+ if (node.hasOwnProperty("priceData")) {
59
+ node.schedule = runBuySellAlgorithm(
60
+ node.priceData,
61
+ node.timeHeat1C,
62
+ node.timeCool1C,
63
+ node.boostTempHeat,
64
+ node.boostTempCool,
65
+ node.maxTempAdjustment,
66
+ node.minSavings
67
+ );
68
+
69
+ if (msg.payload.hasOwnProperty("time")) {
70
+ node.dT = findTemp(msg.payload.time, node.schedule);
71
+ } else {
72
+ node.dT = findTemp(DateTime.now(), node.schedule);
73
+ }
74
+
75
+ node.T = node.setpoint + node.dT;
76
+
77
+ //Add config to statistics
78
+ node.schedule.config = {
79
+ timeHeat1C: node.timeHeat1C,
80
+ timeCool1C: node.timeCool1C,
81
+ setpoint: node.setpoint,
82
+ maxTempAdjustment: node.maxTempAdjustment,
83
+ boostTempHeat: node.boostTempHeat,
84
+ boostTempCool: node.boostTempCool,
85
+ minSavings: node.minSavings,
86
+ };
87
+
88
+ node.schedule.priceData = node.priceData;
89
+ node.schedule.time = DateTime.now().toISO();
90
+ node.schedule.version = version;
91
+
92
+ // Send output
93
+ node.send([
94
+ { payload: node.T, topic: "setpoint", time: node.schedule.time, version: version },
95
+ { payload: node.dT, topic: "adjustment", time: node.schedule.time, version: version },
96
+ { payload: node.schedule },
97
+ ]);
98
+ }
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ RED.nodes.registerType("ps-strategy-heat-capacitor", TempMan);
105
+ };
106
+
107
+ function mergePriceData(priceDataA, priceDataB) {
108
+ const tempDict = {};
109
+ priceDataA.forEach((e) => {
110
+ tempDict[e.start] = e.value;
111
+ });
112
+ priceDataB.forEach((e) => {
113
+ tempDict[e.start] = e.value;
114
+ });
115
+
116
+ var keys = Object.keys(tempDict);
117
+ keys.sort();
118
+
119
+ const res = Array(keys.length);
120
+ for (let i = 0; i < res.length; i++) {
121
+ res[i] = { value: tempDict[keys[i]], start: keys[i] };
122
+ }
123
+
124
+ return res;
125
+ }
@@ -194,4 +194,4 @@
194
194
  "start": "2021-10-12T23:00:00.000+02:00"
195
195
  }
196
196
  ]
197
- }
197
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "source": "Tibber",
3
+ "priceData": [
4
+ { "value": 1.4157, "start": "2022-02-06T00:00:00.000+01:00" },
5
+ { "value": 1.3705, "start": "2022-02-06T01:00:00.000+01:00" },
6
+ { "value": 1.2487, "start": "2022-02-06T02:00:00.000+01:00" },
7
+ { "value": 1.1812, "start": "2022-02-06T03:00:00.000+01:00" },
8
+ { "value": 0.9632, "start": "2022-02-06T04:00:00.000+01:00" },
9
+ { "value": 1.0862, "start": "2022-02-06T05:00:00.000+01:00" },
10
+ { "value": 1.2638, "start": "2022-02-06T06:00:00.000+01:00" },
11
+ { "value": 1.3266, "start": "2022-02-06T07:00:00.000+01:00" },
12
+ { "value": 1.3734, "start": "2022-02-06T08:00:00.000+01:00" },
13
+ { "value": 1.4653, "start": "2022-02-06T09:00:00.000+01:00" },
14
+ { "value": 1.513, "start": "2022-02-06T10:00:00.000+01:00" },
15
+ { "value": 1.518, "start": "2022-02-06T11:00:00.000+01:00" },
16
+ { "value": 1.505, "start": "2022-02-06T12:00:00.000+01:00" },
17
+ { "value": 1.5038, "start": "2022-02-06T13:00:00.000+01:00" },
18
+ { "value": 1.5239, "start": "2022-02-06T14:00:00.000+01:00" },
19
+ { "value": 1.5269, "start": "2022-02-06T15:00:00.000+01:00" },
20
+ { "value": 1.5464, "start": "2022-02-06T16:00:00.000+01:00" },
21
+ { "value": 1.5553, "start": "2022-02-06T17:00:00.000+01:00" },
22
+ { "value": 1.6066, "start": "2022-02-06T18:00:00.000+01:00" },
23
+ { "value": 1.5541, "start": "2022-02-06T19:00:00.000+01:00" },
24
+ { "value": 1.5437, "start": "2022-02-06T20:00:00.000+01:00" },
25
+ { "value": 1.4912, "start": "2022-02-06T21:00:00.000+01:00" },
26
+ { "value": 1.473, "start": "2022-02-06T22:00:00.000+01:00" },
27
+ { "value": 1.4281, "start": "2022-02-06T23:00:00.000+01:00" },
28
+ { "value": 1.4179, "start": "2022-02-07T00:00:00.000+01:00" },
29
+ { "value": 1.3966, "start": "2022-02-07T01:00:00.000+01:00" },
30
+ { "value": 1.3953, "start": "2022-02-07T02:00:00.000+01:00" },
31
+ { "value": 1.3939, "start": "2022-02-07T03:00:00.000+01:00" },
32
+ { "value": 1.3962, "start": "2022-02-07T04:00:00.000+01:00" },
33
+ { "value": 1.4335, "start": "2022-02-07T05:00:00.000+01:00" },
34
+ { "value": 1.5191, "start": "2022-02-07T06:00:00.000+01:00" },
35
+ { "value": 1.6244, "start": "2022-02-07T07:00:00.000+01:00" },
36
+ { "value": 1.6508, "start": "2022-02-07T08:00:00.000+01:00" },
37
+ { "value": 1.6142, "start": "2022-02-07T09:00:00.000+01:00" },
38
+ { "value": 1.5632, "start": "2022-02-07T10:00:00.000+01:00" },
39
+ { "value": 1.5516, "start": "2022-02-07T11:00:00.000+01:00" },
40
+ { "value": 1.4922, "start": "2022-02-07T12:00:00.000+01:00" },
41
+ { "value": 1.5288, "start": "2022-02-07T13:00:00.000+01:00" },
42
+ { "value": 1.5522, "start": "2022-02-07T14:00:00.000+01:00" },
43
+ { "value": 1.5526, "start": "2022-02-07T15:00:00.000+01:00" },
44
+ { "value": 1.5799, "start": "2022-02-07T16:00:00.000+01:00" },
45
+ { "value": 1.6909, "start": "2022-02-07T17:00:00.000+01:00" },
46
+ { "value": 1.7136, "start": "2022-02-07T18:00:00.000+01:00" },
47
+ { "value": 1.703, "start": "2022-02-07T19:00:00.000+01:00" },
48
+ { "value": 1.675, "start": "2022-02-07T20:00:00.000+01:00" },
49
+ { "value": 1.6173, "start": "2022-02-07T21:00:00.000+01:00" },
50
+ { "value": 1.5638, "start": "2022-02-07T22:00:00.000+01:00" },
51
+ { "value": 1.4645, "start": "2022-02-07T23:00:00.000+01:00" }
52
+ ]
53
+ }