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.
- package/.firebase/hosting.ZG9jcy8udnVlcHJlc3MvZGlzdA.cache +94 -0
- package/.firebaserc +5 -0
- package/.github/workflows/firebase-hosting-merge.yml +20 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +17 -0
- package/README.md +1 -1
- package/docs/.vuepress/components/BestSaveVerificator.vue +3 -3
- package/docs/.vuepress/config.js +15 -1
- package/docs/.vuepress/dist/404.html +23 -5
- package/docs/.vuepress/dist/assets/css/896.styles.21a80cb6.css +1 -0
- package/docs/.vuepress/dist/assets/css/styles.1c48cbd0.css +10 -0
- package/docs/.vuepress/dist/assets/img/heat-capacitor-temperatureVsPrice.6e74905b.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-strategy-heat-capacitor-cascade-control.2e75ed9e.png +0 -0
- package/docs/.vuepress/dist/assets/img/node-ps-strategy-heat-capacitor-simple-flow-example.29d9bf59.png +0 -0
- package/docs/.vuepress/dist/assets/img/oven-setpoint-calculation.5bda0eec.png +0 -0
- package/docs/.vuepress/dist/assets/img/overshoot-time.b3b5d70e.png +0 -0
- package/docs/.vuepress/dist/assets/js/229.5c5378fa.js +1 -0
- package/docs/.vuepress/dist/assets/js/331.872104cd.js +1 -0
- package/docs/.vuepress/dist/assets/js/405.f4edd94d.js +2 -0
- package/docs/.vuepress/dist/assets/js/{619.8ba1b1f6.js.LICENSE.txt → 405.f4edd94d.js.LICENSE.txt} +0 -0
- package/docs/.vuepress/dist/assets/js/490.1e639e05.js +1 -0
- package/docs/.vuepress/dist/assets/js/{491.17a98f38.js → 491.bd938119.js} +1 -1
- package/docs/.vuepress/dist/assets/js/555.d8963d84.js +1 -0
- package/docs/.vuepress/dist/assets/js/{811.6a3392d5.js → 811.5f659592.js} +0 -0
- package/docs/.vuepress/dist/assets/js/app.dfdee6f9.js +1 -0
- package/docs/.vuepress/dist/assets/js/runtime~app.f6ac32d7.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0607240a.0193a377.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-08683c60.52e94cb6.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0aca7ba6.cac5d4b9.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.18561f6e.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1ad821fa.6697a349.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1b3a0ab8.c6c4e19b.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-1e2b191e.07b8ab21.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-29504124.00be7399.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-30acb564.28af12af.js +1 -0
- package/docs/.vuepress/dist/assets/js/{v-3706649a.d7f73384.js → v-3706649a.c76d575b.js} +1 -1
- package/docs/.vuepress/dist/assets/js/v-4637f9e4.d334c29a.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-4c28314d.8cbb0f9d.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-510ed0d4.c04bc2e4.js +1 -0
- package/docs/.vuepress/dist/assets/js/{v-5954bcb2.937005d0.js → v-5954bcb2.dff3fc67.js} +1 -1
- package/docs/.vuepress/dist/assets/js/v-5db8da3a.e5e6d7a6.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-61f728ca.81968036.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-677dfaed.c159b0f4.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-7446a652.8fc2c591.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-7c87f26e.8ed52391.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-84304104.f3f07ed3.js +1 -0
- package/docs/.vuepress/dist/assets/js/{v-8daa1a0e.c63afc2b.js → v-8daa1a0e.ed84ca09.js} +1 -1
- package/docs/.vuepress/dist/assets/js/{v-b4a42144.733e4e7c.js → v-b4a42144.9a2a0c9f.js} +1 -1
- package/docs/.vuepress/dist/assets/js/v-e8c55052.b7d52fc6.js +1 -0
- package/docs/.vuepress/dist/assets/js/v-fffb8e28.d09ab959.js +1 -0
- package/docs/.vuepress/dist/changelog/index.html +23 -5
- package/docs/.vuepress/dist/contribute/index.html +23 -5
- package/docs/.vuepress/dist/examples/example-cascade-temperature-control.html +304 -0
- package/docs/.vuepress/dist/examples/example-heat-capacitor.html +247 -0
- package/docs/.vuepress/dist/examples/example-next-schedule-entity.html +27 -9
- package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +24 -6
- package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +24 -6
- package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +24 -6
- package/docs/.vuepress/dist/examples/index.html +23 -5
- package/docs/.vuepress/dist/faq/best-save-viewer.html +23 -5
- package/docs/.vuepress/dist/faq/index.html +23 -5
- package/docs/.vuepress/dist/guide/index.html +24 -6
- package/docs/.vuepress/dist/index.html +23 -5
- package/docs/.vuepress/dist/nodes/index.html +23 -5
- package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +25 -7
- package/docs/.vuepress/dist/nodes/power-saver.html +23 -5
- package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +23 -5
- package/docs/.vuepress/dist/nodes/ps-general-add-tariff.html +23 -5
- package/docs/.vuepress/dist/nodes/ps-receive-price.html +26 -8
- package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +32 -9
- package/docs/.vuepress/dist/nodes/ps-strategy-heat-capacitor.html +260 -0
- package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +30 -7
- package/docs/.vuepress/dist/nodes/strategy-input.html +24 -6
- package/docs/README.md +1 -1
- package/docs/changelog/README.md +14 -0
- package/docs/contribute/README.md +8 -0
- package/docs/examples/README.md +8 -2
- package/docs/examples/example-cascade-temperature-control.md +346 -0
- package/docs/examples/example-heat-capacitor.md +271 -0
- package/docs/images/heat-capacitor-temperatureVsPrice.png +0 -0
- package/docs/images/node-ps-strategy-heat-capacitor-cascade-control.png +0 -0
- package/docs/images/node-ps-strategy-heat-capacitor-simple-flow-example.png +0 -0
- package/docs/images/node-ps-strategy-heat-capacitor.png +0 -0
- package/docs/images/oven-setpoint-calculation.png +0 -0
- package/docs/images/overshoot-time.png +0 -0
- package/docs/images/temperature-manipulation-config.png +0 -0
- package/docs/nodes/README.md +7 -1
- package/docs/nodes/ps-strategy-heat-capacitor.md +346 -0
- package/examples/add-general-tariff.json +103 -0
- package/examples/best-save-for-water-heater.json +140 -0
- package/examples/elvia-add-tariff.json +99 -0
- package/examples/elvia-get-tariff-types.json +58 -0
- package/examples/elvia-get-tariff.json +60 -0
- package/examples/heat-capacitor-for-room-heating.json +186 -0
- package/examples/lowest-price-for-heating-cables.json +159 -0
- package/firebase.json +6 -0
- package/package.json +17 -9
- package/public/404.html +33 -0
- package/public/index.html +89 -0
- package/src/elvia/elvia-tariff.js +2 -1
- package/src/handle-input.js +6 -3
- package/src/strategy-heat-capacitor-functions.js +246 -0
- package/src/strategy-heat-capacitor.html +85 -0
- package/src/strategy-heat-capacitor.js +125 -0
- package/test/data/converted-prices.json +1 -1
- package/test/data/multiple-trades.json +53 -0
- package/test/data/tibber-decreasing-24h.json +101 -0
- package/test/data/tibber-decreasing2-24h.json +101 -0
- package/test/strategy-heat-capacitor-node.test.js +183 -0
- package/test/strategy-heat-capacitor.test.js +103 -0
- package/test/strategy-lowest-price-functions.test.js +1 -1
- package/test/utils.test.js +0 -2
- package/docs/.vuepress/dist/.nojekyll +0 -0
- package/docs/.vuepress/dist/assets/css/563.styles.99f4a8aa.css +0 -1
- package/docs/.vuepress/dist/assets/css/styles.031dcf27.css +0 -9
- package/docs/.vuepress/dist/assets/js/262.cf2c57d2.js +0 -1
- package/docs/.vuepress/dist/assets/js/293.08ea5200.js +0 -1
- package/docs/.vuepress/dist/assets/js/331.15ee3c51.js +0 -1
- package/docs/.vuepress/dist/assets/js/619.8ba1b1f6.js +0 -2
- package/docs/.vuepress/dist/assets/js/app.b705176c.js +0 -1
- package/docs/.vuepress/dist/assets/js/runtime~app.47f4f812.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-0607240a.a57c2199.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-08683c60.ccafdcab.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-0aca7ba6.25903946.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-0b5e3c8c.a6a015b4.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-1ad821fa.5978386f.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-1e2b191e.88dc5555.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-29504124.4aca27d5.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-30acb564.529a3c16.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-4637f9e4.703b1d96.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-510ed0d4.7b142a81.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-5db8da3a.3de3588d.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-61f728ca.21d432fe.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-677dfaed.44a653b9.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-7446a652.74b21d0b.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-7c87f26e.ee5be992.js +0 -1
- package/docs/.vuepress/dist/assets/js/v-e8c55052.ab0a79ec.js +0 -1
- 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…</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>
|
package/src/handle-input.js
CHANGED
|
@@ -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:
|
|
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().
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|