node-red-contrib-power-saver 5.0.0-beta.4 → 5.0.0-beta.6

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.
@@ -3,21 +3,22 @@ name: Publish to npmjs
3
3
  on:
4
4
  release:
5
5
  types: [created]
6
+ permissions:
7
+ id-token: write
8
+ contents: read
6
9
  jobs:
7
10
  build:
8
11
  runs-on: ubuntu-latest
9
12
  steps:
10
- - uses: actions/checkout@v3
13
+ - uses: actions/checkout@v4
11
14
  # Setup .npmrc file to publish to npm
12
- - uses: actions/setup-node@v3
15
+ - uses: actions/setup-node@v4
13
16
  with:
14
- node-version: '16.x'
17
+ node-version: '24.x'
15
18
  registry-url: 'https://registry.npmjs.org'
16
19
  - uses: szenius/set-timezone@v1.0
17
20
  with:
18
21
  timezoneLinux: "Europe/Oslo"
19
22
  - run: npm ci
20
23
  - run: 'npm run test'
21
- - run: npm publish
22
- env:
23
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
24
+ - run: npm publish --tag beta --provenance
@@ -7,6 +7,16 @@ sidebarDepth: 1
7
7
 
8
8
  List the most significant changes.
9
9
 
10
+ ## 5.0.0.beta.6
11
+
12
+ - Best Save Performance improvement
13
+
14
+ ## 5.0.0.beta.5
15
+
16
+ - Fix Lowest Price so on-periods are not split up if they can go together
17
+ - Collapse minutes array on output 3 (NB! BREAKING CHANGE)
18
+ - Remove log line "Switching off in x milliseconds"
19
+
10
20
  ## 5.0.0.beta.4
11
21
 
12
22
  - Fix bug on recovery time when recoveryMaxMinutes is not set
@@ -135,23 +135,40 @@ Example of output:
135
135
  }
136
136
  ],
137
137
  "minutes": [
138
- {
138
+ {
139
139
  "start": "2025-09-30T00:00:00.000+02:00",
140
140
  "price": 0.2129,
141
141
  "onOff": false,
142
- "saving": 0.0631
142
+ "saving": 0.0631,
143
+ "count": 15
143
144
  },
144
145
  {
145
- "start": "2025-09-30T00:01:00.000+02:00",
146
- "price": 0.2129,
146
+ "start": "2025-09-30T00:15:00.000+02:00",
147
+ "price": 0.2127,
147
148
  "onOff": false,
148
- "saving": 0.0631
149
+ "saving": 0.0633,
150
+ "count": 15
149
151
  },
150
152
  {
151
- "start": "2025-09-30T00:02:00.000+02:00",
152
- "price": 0.2129,
153
+ "start": "2025-09-30T00:30:00.000+02:00",
154
+ "price": 0.2231,
153
155
  "onOff": false,
154
- "saving": 0.0631
156
+ "saving": 0.0529,
157
+ "count": 15
158
+ },
159
+ {
160
+ "start": "2025-09-30T00:45:00.000+02:00",
161
+ "price": 0.2235,
162
+ "onOff": false,
163
+ "saving": 0.0533,
164
+ "count": 15
165
+ },
166
+ {
167
+ "start": "2025-09-30T01:00:00.000+02:00",
168
+ "price": 0.2760,
169
+ "onOff": true,
170
+ "saving": null,
171
+ "count": 1380
155
172
  }, // ...
156
173
  ],
157
174
  "source": "Nord Pool",
@@ -181,20 +181,23 @@ Example of output:
181
181
  "start": "2025-10-07T00:00:00.000+02:00",
182
182
  "price": 0.1875,
183
183
  "onOff": false,
184
- "saving": null
184
+ "saving": null,
185
+ "count": 15
185
186
  },
186
187
  {
187
- "start": "2025-10-07T00:01:00.000+02:00",
188
- "price": 0.1875,
188
+ "start": "2025-10-07T00:15:00.000+02:00",
189
+ "price": 0.1882,
189
190
  "onOff": false,
190
- "saving": null
191
+ "saving": null,
192
+ "count": 15
191
193
  },
192
194
  //...
193
195
  {
194
- "start": "2025-10-07T16:39:00.000+02:00",
196
+ "start": "2025-10-08T23:45:00.000+02:00",
195
197
  "price": 0.2578,
196
198
  "onOff": false,
197
- "saving": null
199
+ "saving": null,
200
+ "count": 15
198
201
  }
199
202
  ],
200
203
  "source": "Tibber",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-saver",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.6",
4
4
  "description": "A module for Node-RED that you can use to turn on and off a switch based on power prices",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -20,7 +20,7 @@ function handleOutput(node, config, plan, outputCommands, planFromTime) {
20
20
  let output3 = {
21
21
  payload: {
22
22
  schedule: plan.schedule,
23
- minutes: plan.minutes,
23
+ minutes: collapseMinutes(plan.minutes),
24
24
  source: plan.source,
25
25
  config,
26
26
  time: planFromTime.toISO(),
@@ -74,7 +74,6 @@ function runSchedule(node, schedule, time, currentSent = false) {
74
74
  const nextTime = DateTime.fromISO(entry.time);
75
75
  const wait = nextTime - time;
76
76
  const onOff = entry.value ? "on" : "off";
77
- node.log("Switching " + onOff + " in " + wait + " milliseconds");
78
77
  const statusMessage = `${remainingSchedule.length} changes - ${
79
78
  remainingSchedule[0].value ? "on" : "off"
80
79
  } at ${nextTime.toFormat("HH:mm")}`;
@@ -110,6 +109,42 @@ function strategyShallSendSchedule(msg, commands) {
110
109
  return msgHasConfig(msg) || msgHasPriceData(msg) || commands.replan;
111
110
  }
112
111
 
112
+ function collapseMinutes(minutes) {
113
+ function itemsEqual(a, b) {
114
+ return a.price === b.price && a.onOff === b.onOff && a.saving === b.saving;
115
+ }
116
+
117
+
118
+
119
+
120
+ if (!Array.isArray(minutes) || minutes.length === 0) {
121
+ return [];
122
+ }
123
+
124
+ const result = [];
125
+ let currentValue = minutes[0];
126
+ let count = 1;
127
+ let startIndex = 0;
128
+
129
+ for (let i = 1; i < minutes.length; i++) {
130
+ if (itemsEqual(minutes[i], currentValue)) {
131
+ count++;
132
+ } else {
133
+ result.push({ ...currentValue, count, startIndex });
134
+ currentValue = minutes[i];
135
+ count = 1;
136
+ startIndex = i;
137
+ }
138
+ }
139
+
140
+ result.push({ ...currentValue, count, startIndex });
141
+
142
+ return result;
143
+
144
+
145
+
146
+ }
147
+
113
148
  module.exports = {
114
149
  handleOutput,
115
150
  shallSendOutput,
@@ -13,12 +13,7 @@ const { fillArray } = require("./utils");
13
13
  * @param {*} recoveryMaxMinutes Maximum recovery time in minutes
14
14
  * @returns
15
15
  */
16
- function isOnOffSequencesOk(
17
- onOff,
18
- maxMinutesOff,
19
- minMinutesOff,
20
- recoveryPercentage,
21
- recoveryMaxMinutes = null) {
16
+ function isOnOffSequencesOk(onOff, maxMinutesOff, minMinutesOff, recoveryPercentage, recoveryMaxMinutes = null) {
22
17
  let offCount = 0;
23
18
  let onCount = 0;
24
19
  let reachedMaxOff = false;
@@ -30,10 +25,10 @@ function isOnOffSequencesOk(
30
25
  if (maxMinutesOff === 0 || reachedMaxOff) {
31
26
  return false;
32
27
  }
33
- if(!reachedMinOn) {
28
+ if (!reachedMinOn) {
34
29
  return false;
35
30
  }
36
- if(reachedMinOff === null) {
31
+ if (reachedMinOff === null) {
37
32
  reachedMinOff = false;
38
33
  }
39
34
  offCount++;
@@ -44,16 +39,16 @@ function isOnOffSequencesOk(
44
39
  if (offCount >= minMinutesOff) {
45
40
  reachedMinOff = true;
46
41
  }
47
- const minRounded = Math.max(Math.round(offCount * recoveryPercentage / 100), 1)
42
+ const minRounded = Math.max(Math.round((offCount * recoveryPercentage) / 100), 1);
48
43
  const recMaxMin = recoveryMaxMinutes === "" ? null : recoveryMaxMinutes;
49
- minOnAfterOff = Math.min(minRounded, recMaxMin ?? minRounded)
50
- if(i === onOff.length - 1) {
44
+ minOnAfterOff = Math.min(minRounded, recMaxMin ?? minRounded);
45
+ if (i === onOff.length - 1) {
51
46
  // If last minute, consider min reached
52
47
  reachedMinOn = true;
53
48
  reachedMinOff = true;
54
49
  }
55
50
  } else {
56
- if(reachedMinOff === false) {
51
+ if (reachedMinOff === false) {
57
52
  return false;
58
53
  }
59
54
  onCount++;
@@ -123,12 +118,12 @@ function calculate(
123
118
  }
124
119
  }
125
120
 
126
- savingsList.sort((a, b) => b.saving === a.saving ? a.count - b.count : b.saving - a.saving);
121
+ savingsList.sort((b, a) => (b.saving === a.saving ? a.count - b.count : b.saving - a.saving));
127
122
  let onOff = values.map((v) => true); // Start with all on
128
123
 
129
124
  // Find the best possible sequences
130
125
  while (savingsList.length > 0) {
131
- const { minute, count } = savingsList[0];
126
+ const { minute, count } = savingsList[savingsList.length - 1];
132
127
  const onOffCopy = [...onOff];
133
128
  let alreadyTaken = false;
134
129
  for (let c = 0; c < count; c++) {
@@ -137,12 +132,11 @@ function calculate(
137
132
  }
138
133
  onOff[minute + c] = false;
139
134
  }
140
- if (isOnOffSequencesOk([...dayBefore, ...onOff], maxMinutesOff, minMinutesOff, recoveryPercentage,
141
- recoveryMaxMinutes) && !alreadyTaken) {
135
+ if ( isOnOffSequencesOk( [...dayBefore, ...onOff], maxMinutesOff, minMinutesOff, recoveryPercentage, recoveryMaxMinutes ) && !alreadyTaken ) {
142
136
  savingsList = savingsList.filter((s) => s.minute < minute || s.minute >= minute + count);
143
137
  } else {
144
138
  onOff = [...onOffCopy];
145
- savingsList.splice(0, 1);
139
+ savingsList.pop();
146
140
  }
147
141
  }
148
142
  return onOff;
package/src/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ const cloneDeep = require("lodash.clonedeep");
1
2
  const { DateTime } = require("luxon");
2
3
 
3
4
  function booleanConfig(value) {
@@ -24,19 +25,133 @@ function saveOriginalConfig(node, originalConfig) {
24
25
  * in sorted order. Highest value first.
25
26
  */
26
27
  function sortedIndex(valueArr) {
27
- const mapped = valueArr.map((v, i) => {
28
- return { i, value: v };
28
+ const collapsed = collapseArr(valueArr);
29
+ const withNeighbours = addNeighbours(collapsed);
30
+ const sortedCollapsed = sortCollapsed(withNeighbours);
31
+ const res = [];
32
+ sortedCollapsed.forEach((group) => {
33
+ const start = group.internalOrder === "asc" ? 0 : group.count - 1;;
34
+ const end = group.internalOrder === "asc" ? group.count : -1;
35
+ const step = group.internalOrder === "asc" ? 1 : -1;
36
+ for (let j = start; j !== end; j += step) {
37
+ res.push(group.startIndex + j);
38
+ }
39
+ });
40
+ return res;
41
+ }
42
+
43
+
44
+ /**
45
+ * The valueArr contains values.
46
+ * Collapse consecutive same values into one.
47
+ * Return array with the collapsed values and their counts.
48
+ *
49
+ * @param {*} valueArr
50
+ */
51
+ function collapseArr(valueArr, itemsEqual = (a, b) => a === b) {
52
+ if (!Array.isArray(valueArr) || valueArr.length === 0) {
53
+ return [];
54
+ }
55
+
56
+ const result = [];
57
+ let currentValue = valueArr[0];
58
+ let count = 1;
59
+ let startIndex = 0;
60
+
61
+ for (let i = 1; i < valueArr.length; i++) {
62
+ if (itemsEqual(valueArr[i], currentValue)) {
63
+ count++;
64
+ } else {
65
+ result.push({ value: currentValue, count, startIndex });
66
+ currentValue = valueArr[i];
67
+ count = 1;
68
+ startIndex = i;
69
+ }
70
+ }
71
+
72
+ result.push({ value: currentValue, count, startIndex });
73
+
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Takes a collapsed array (from collapseArr)
79
+ * and expands it back to the original value array.
80
+ *
81
+ * @param {*} collapsedArr
82
+ */
83
+ function expandArr(collapsedArr) {
84
+ const result = [];
85
+
86
+ for (const { value, count } of collapsedArr) {
87
+ for (let i = 0; i < count; i++) {
88
+ result.push(value);
89
+ }
90
+ }
91
+
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Take a collapsed array as input.
97
+ * Add a property 'before' and 'after' to each item:
98
+ * containing the value before and after in the original array,
99
+ * or null if there is none.
100
+ *
101
+ * @param {*} collapsedArr
102
+ */
103
+ function addNeighbours(collapsedArr) {
104
+ if (!Array.isArray(collapsedArr)) return [];
105
+
106
+ return collapsedArr.map((item, index) => {
107
+ const before = index > 0 ? collapsedArr[index - 1].value : null;
108
+ const after = index < collapsedArr.length - 1 ? collapsedArr[index + 1].value : null;
109
+
110
+ return {
111
+ ...item,
112
+ before,
113
+ after,
114
+ };
29
115
  });
30
- const sorted = mapped.sort((a, b) => {
31
- if (a.value > b.value) {
116
+ }
117
+
118
+ /**
119
+ * Sort records in collapsed array by value descending,
120
+ * then by count ascending, then by best of before and after descending.
121
+ * If before or after is null, null goes first
122
+ */
123
+
124
+ function sortCollapsed(collapsedArr) {
125
+ const sorted = cloneDeep(collapsedArr).sort((a, b) => {
126
+ // 1. value ascending
127
+ if (a.value !== b.value) {
128
+ return b.value - a.value;
129
+ }
130
+
131
+ // 2. count descending
132
+ if (a.count !== b.count) {
133
+ return a.count - b.count;
134
+ }
135
+
136
+ // 3. before or after
137
+ if (a.before === null || a.after === null) {
32
138
  return -1;
33
139
  }
34
- if (a.value < b.value) {
140
+ if (b.before === null || b.after === null) {
35
141
  return 1;
36
142
  }
37
- return 0;
143
+ const aBest = Math.max(a.before, a.after);
144
+ const bBest = Math.max(b.before, b.after);
145
+
146
+ return bBest - aBest;
38
147
  });
39
- return sorted.map((p) => p.i);
148
+
149
+ // Set internal ordering for each line
150
+ sorted.forEach((item) => {
151
+ item.internalOrder =
152
+ item.after === null ? "desc" : item.before === null ? "asc" : item.after > item.before ? "desc" : "asc";
153
+ });
154
+ return sorted
40
155
  }
41
156
 
42
157
  /**
@@ -105,8 +220,8 @@ function loadDayData(node, date) {
105
220
  schedule: [],
106
221
  minutes: [],
107
222
  };
108
- if(!res.minutes) {
109
- res.minutes = []
223
+ if (!res.minutes) {
224
+ res.minutes = [];
110
225
  }
111
226
  return res;
112
227
  }
@@ -173,13 +288,15 @@ function makeSchedule(onOff, startTimes, endTime, initial = null) {
173
288
  res.push(prevRecord);
174
289
  prev = value;
175
290
  }
176
- prevRecord.countMinutes = DateTime.fromISO(i + 1 < startTimes.length ? startTimes[i+1] : endTime).diff(DateTime.fromISO(prevRecord.time), "minutes").minutes;
291
+ prevRecord.countMinutes = DateTime.fromISO(i + 1 < startTimes.length ? startTimes[i + 1] : endTime).diff(
292
+ DateTime.fromISO(prevRecord.time),
293
+ "minutes"
294
+ ).minutes;
177
295
  }
178
296
  return res;
179
297
  }
180
298
 
181
299
  function addEndToLast(priceData) {
182
-
183
300
  // Add end property to the last record, that is the same as start + the difference between the last two starts, converted to ISO time
184
301
 
185
302
  if (priceData.length > 0) {
@@ -192,10 +309,7 @@ function addEndToLast(priceData) {
192
309
  }
193
310
 
194
311
  function makeScheduleFromMinutes(minutes, initial = null) {
195
-
196
- addEndToLast(minutes)
197
-
198
-
312
+ addEndToLast(minutes);
199
313
 
200
314
  return makeSchedule(
201
315
  minutes.map((h) => h.onOff),
@@ -276,8 +390,11 @@ function getOutputForTime(schedule, time, defaultValue) {
276
390
 
277
391
  module.exports = {
278
392
  addEndToLast,
393
+ addNeighbours,
279
394
  booleanConfig,
280
395
  calcNullSavings,
396
+ collapseArr,
397
+ expandArr,
281
398
  countAtEnd,
282
399
  extractPlanForDate,
283
400
  fillArray,
@@ -298,6 +415,7 @@ module.exports = {
298
415
  msgHasPriceData,
299
416
  roundPrice,
300
417
  saveOriginalConfig,
418
+ sortCollapsed,
301
419
  sortedIndex,
302
420
  validationFailure,
303
421
  };
@@ -0,0 +1,197 @@
1
+ module.exports = {
2
+ priceData: [
3
+ { value: 1.1599, start: "2026-01-10T00:00:00.000+01:00" },
4
+ { value: 1.1599, start: "2026-01-10T00:15:00.000+01:00" },
5
+ { value: 1.1599, start: "2026-01-10T00:30:00.000+01:00" },
6
+ { value: 1.1599, start: "2026-01-10T00:45:00.000+01:00" },
7
+ { value: 1.1341, start: "2026-01-10T01:00:00.000+01:00" },
8
+ { value: 1.1341, start: "2026-01-10T01:15:00.000+01:00" },
9
+ { value: 1.1341, start: "2026-01-10T01:30:00.000+01:00" },
10
+ { value: 1.1341, start: "2026-01-10T01:45:00.000+01:00" },
11
+ { value: 1.1122, start: "2026-01-10T02:00:00.000+01:00" },
12
+ { value: 1.1122, start: "2026-01-10T02:15:00.000+01:00" },
13
+ { value: 1.1122, start: "2026-01-10T02:30:00.000+01:00" },
14
+ { value: 1.1122, start: "2026-01-10T02:45:00.000+01:00" },
15
+ { value: 1.0987, start: "2026-01-10T03:00:00.000+01:00" },
16
+ { value: 1.0987, start: "2026-01-10T03:15:00.000+01:00" },
17
+ { value: 1.0987, start: "2026-01-10T03:30:00.000+01:00" },
18
+ { value: 1.0987, start: "2026-01-10T03:45:00.000+01:00" },
19
+ { value: 1.1093, start: "2026-01-10T04:00:00.000+01:00" },
20
+ { value: 1.1093, start: "2026-01-10T04:15:00.000+01:00" },
21
+ { value: 1.1093, start: "2026-01-10T04:30:00.000+01:00" },
22
+ { value: 1.1093, start: "2026-01-10T04:45:00.000+01:00" },
23
+ { value: 1.1234, start: "2026-01-10T05:00:00.000+01:00" },
24
+ { value: 1.1234, start: "2026-01-10T05:15:00.000+01:00" },
25
+ { value: 1.1234, start: "2026-01-10T05:30:00.000+01:00" },
26
+ { value: 1.1234, start: "2026-01-10T05:45:00.000+01:00" },
27
+ { value: 1.1358, start: "2026-01-10T06:00:00.000+01:00" },
28
+ { value: 1.1358, start: "2026-01-10T06:15:00.000+01:00" },
29
+ { value: 1.1358, start: "2026-01-10T06:30:00.000+01:00" },
30
+ { value: 1.1358, start: "2026-01-10T06:45:00.000+01:00" },
31
+ { value: 1.1978, start: "2026-01-10T07:00:00.000+01:00" },
32
+ { value: 1.1978, start: "2026-01-10T07:15:00.000+01:00" },
33
+ { value: 1.1978, start: "2026-01-10T07:30:00.000+01:00" },
34
+ { value: 1.1978, start: "2026-01-10T07:45:00.000+01:00" },
35
+ { value: 1.2682, start: "2026-01-10T08:00:00.000+01:00" },
36
+ { value: 1.2682, start: "2026-01-10T08:15:00.000+01:00" },
37
+ { value: 1.2682, start: "2026-01-10T08:30:00.000+01:00" },
38
+ { value: 1.2682, start: "2026-01-10T08:45:00.000+01:00" },
39
+ { value: 1.3567, start: "2026-01-10T09:00:00.000+01:00" },
40
+ { value: 1.3567, start: "2026-01-10T09:15:00.000+01:00" },
41
+ { value: 1.3567, start: "2026-01-10T09:30:00.000+01:00" },
42
+ { value: 1.3567, start: "2026-01-10T09:45:00.000+01:00" },
43
+ { value: 1.4249, start: "2026-01-10T10:00:00.000+01:00" },
44
+ { value: 1.4249, start: "2026-01-10T10:15:00.000+01:00" },
45
+ { value: 1.4249, start: "2026-01-10T10:30:00.000+01:00" },
46
+ { value: 1.4249, start: "2026-01-10T10:45:00.000+01:00" },
47
+ { value: 1.4814, start: "2026-01-10T11:00:00.000+01:00" },
48
+ { value: 1.4814, start: "2026-01-10T11:15:00.000+01:00" },
49
+ { value: 1.4814, start: "2026-01-10T11:30:00.000+01:00" },
50
+ { value: 1.4814, start: "2026-01-10T11:45:00.000+01:00" },
51
+ { value: 1.4815, start: "2026-01-10T12:00:00.000+01:00" },
52
+ { value: 1.4815, start: "2026-01-10T12:15:00.000+01:00" },
53
+ { value: 1.4815, start: "2026-01-10T12:30:00.000+01:00" },
54
+ { value: 1.4815, start: "2026-01-10T12:45:00.000+01:00" },
55
+ { value: 1.5304, start: "2026-01-10T13:00:00.000+01:00" },
56
+ { value: 1.5304, start: "2026-01-10T13:15:00.000+01:00" },
57
+ { value: 1.5304, start: "2026-01-10T13:30:00.000+01:00" },
58
+ { value: 1.5304, start: "2026-01-10T13:45:00.000+01:00" },
59
+ { value: 1.6047, start: "2026-01-10T14:00:00.000+01:00" },
60
+ { value: 1.6047, start: "2026-01-10T14:15:00.000+01:00" },
61
+ { value: 1.6047, start: "2026-01-10T14:30:00.000+01:00" },
62
+ { value: 1.6047, start: "2026-01-10T14:45:00.000+01:00" },
63
+ { value: 1.703, start: "2026-01-10T15:00:00.000+01:00" },
64
+ { value: 1.703, start: "2026-01-10T15:15:00.000+01:00" },
65
+ { value: 1.703, start: "2026-01-10T15:30:00.000+01:00" },
66
+ { value: 1.703, start: "2026-01-10T15:45:00.000+01:00" },
67
+ { value: 1.8582, start: "2026-01-10T16:00:00.000+01:00" },
68
+ { value: 1.8582, start: "2026-01-10T16:15:00.000+01:00" },
69
+ { value: 1.8582, start: "2026-01-10T16:30:00.000+01:00" },
70
+ { value: 1.8582, start: "2026-01-10T16:45:00.000+01:00" },
71
+ { value: 2.0223, start: "2026-01-10T17:00:00.000+01:00" },
72
+ { value: 2.0223, start: "2026-01-10T17:15:00.000+01:00" },
73
+ { value: 2.0223, start: "2026-01-10T17:30:00.000+01:00" },
74
+ { value: 2.0223, start: "2026-01-10T17:45:00.000+01:00" },
75
+ { value: 1.985, start: "2026-01-10T18:00:00.000+01:00" },
76
+ { value: 1.985, start: "2026-01-10T18:15:00.000+01:00" },
77
+ { value: 1.985, start: "2026-01-10T18:30:00.000+01:00" },
78
+ { value: 1.985, start: "2026-01-10T18:45:00.000+01:00" },
79
+ { value: 1.8633, start: "2026-01-10T19:00:00.000+01:00" },
80
+ { value: 1.8633, start: "2026-01-10T19:15:00.000+01:00" },
81
+ { value: 1.8633, start: "2026-01-10T19:30:00.000+01:00" },
82
+ { value: 1.8633, start: "2026-01-10T19:45:00.000+01:00" },
83
+ { value: 1.7377, start: "2026-01-10T20:00:00.000+01:00" },
84
+ { value: 1.7377, start: "2026-01-10T20:15:00.000+01:00" },
85
+ { value: 1.7377, start: "2026-01-10T20:30:00.000+01:00" },
86
+ { value: 1.7377, start: "2026-01-10T20:45:00.000+01:00" },
87
+ { value: 1.6171, start: "2026-01-10T21:00:00.000+01:00" },
88
+ { value: 1.6171, start: "2026-01-10T21:15:00.000+01:00" },
89
+ { value: 1.6171, start: "2026-01-10T21:30:00.000+01:00" },
90
+ { value: 1.6171, start: "2026-01-10T21:45:00.000+01:00" },
91
+ { value: 1.4753, start: "2026-01-10T22:00:00.000+01:00" },
92
+ { value: 1.4753, start: "2026-01-10T22:15:00.000+01:00" },
93
+ { value: 1.4753, start: "2026-01-10T22:30:00.000+01:00" },
94
+ { value: 1.4753, start: "2026-01-10T22:45:00.000+01:00" },
95
+ { value: 1.328, start: "2026-01-10T23:00:00.000+01:00" },
96
+ { value: 1.328, start: "2026-01-10T23:15:00.000+01:00" },
97
+ { value: 1.328, start: "2026-01-10T23:30:00.000+01:00" },
98
+ { value: 1.328, start: "2026-01-10T23:45:00.000+01:00" },
99
+ { value: 1.2678, start: "2026-01-11T00:00:00.000+01:00" },
100
+ { value: 1.2678, start: "2026-01-11T00:15:00.000+01:00" },
101
+ { value: 1.2678, start: "2026-01-11T00:30:00.000+01:00" },
102
+ { value: 1.2678, start: "2026-01-11T00:45:00.000+01:00" },
103
+ { value: 1.2745, start: "2026-01-11T01:00:00.000+01:00" },
104
+ { value: 1.2745, start: "2026-01-11T01:15:00.000+01:00" },
105
+ { value: 1.2745, start: "2026-01-11T01:30:00.000+01:00" },
106
+ { value: 1.2745, start: "2026-01-11T01:45:00.000+01:00" },
107
+ { value: 1.1884, start: "2026-01-11T02:00:00.000+01:00" },
108
+ { value: 1.1884, start: "2026-01-11T02:15:00.000+01:00" },
109
+ { value: 1.1884, start: "2026-01-11T02:30:00.000+01:00" },
110
+ { value: 1.1884, start: "2026-01-11T02:45:00.000+01:00" },
111
+ { value: 1.1922, start: "2026-01-11T03:00:00.000+01:00" },
112
+ { value: 1.1922, start: "2026-01-11T03:15:00.000+01:00" },
113
+ { value: 1.1922, start: "2026-01-11T03:30:00.000+01:00" },
114
+ { value: 1.1922, start: "2026-01-11T03:45:00.000+01:00" },
115
+ { value: 1.1889, start: "2026-01-11T04:00:00.000+01:00" },
116
+ { value: 1.1889, start: "2026-01-11T04:15:00.000+01:00" },
117
+ { value: 1.1889, start: "2026-01-11T04:30:00.000+01:00" },
118
+ { value: 1.1889, start: "2026-01-11T04:45:00.000+01:00" },
119
+ { value: 1.2024, start: "2026-01-11T05:00:00.000+01:00" },
120
+ { value: 1.2024, start: "2026-01-11T05:15:00.000+01:00" },
121
+ { value: 1.2024, start: "2026-01-11T05:30:00.000+01:00" },
122
+ { value: 1.2024, start: "2026-01-11T05:45:00.000+01:00" },
123
+ { value: 1.1888, start: "2026-01-11T06:00:00.000+01:00" },
124
+ { value: 1.1888, start: "2026-01-11T06:15:00.000+01:00" },
125
+ { value: 1.1888, start: "2026-01-11T06:30:00.000+01:00" },
126
+ { value: 1.1888, start: "2026-01-11T06:45:00.000+01:00" },
127
+ { value: 1.2164, start: "2026-01-11T07:00:00.000+01:00" },
128
+ { value: 1.2164, start: "2026-01-11T07:15:00.000+01:00" },
129
+ { value: 1.2164, start: "2026-01-11T07:30:00.000+01:00" },
130
+ { value: 1.2164, start: "2026-01-11T07:45:00.000+01:00" },
131
+ { value: 1.2953, start: "2026-01-11T08:00:00.000+01:00" },
132
+ { value: 1.2953, start: "2026-01-11T08:15:00.000+01:00" },
133
+ { value: 1.2953, start: "2026-01-11T08:30:00.000+01:00" },
134
+ { value: 1.2953, start: "2026-01-11T08:45:00.000+01:00" },
135
+ { value: 1.4772, start: "2026-01-11T09:00:00.000+01:00" },
136
+ { value: 1.4772, start: "2026-01-11T09:15:00.000+01:00" },
137
+ { value: 1.4772, start: "2026-01-11T09:30:00.000+01:00" },
138
+ { value: 1.4772, start: "2026-01-11T09:45:00.000+01:00" },
139
+ { value: 1.467, start: "2026-01-11T10:00:00.000+01:00" },
140
+ { value: 1.467, start: "2026-01-11T10:15:00.000+01:00" },
141
+ { value: 1.467, start: "2026-01-11T10:30:00.000+01:00" },
142
+ { value: 1.467, start: "2026-01-11T10:45:00.000+01:00" },
143
+ { value: 1.4597, start: "2026-01-11T11:00:00.000+01:00" },
144
+ { value: 1.4597, start: "2026-01-11T11:15:00.000+01:00" },
145
+ { value: 1.4597, start: "2026-01-11T11:30:00.000+01:00" },
146
+ { value: 1.4597, start: "2026-01-11T11:45:00.000+01:00" },
147
+ { value: 1.4224, start: "2026-01-11T12:00:00.000+01:00" },
148
+ { value: 1.4224, start: "2026-01-11T12:15:00.000+01:00" },
149
+ { value: 1.4224, start: "2026-01-11T12:30:00.000+01:00" },
150
+ { value: 1.4224, start: "2026-01-11T12:45:00.000+01:00" },
151
+ { value: 1.4143, start: "2026-01-11T13:00:00.000+01:00" },
152
+ { value: 1.4143, start: "2026-01-11T13:15:00.000+01:00" },
153
+ { value: 1.4143, start: "2026-01-11T13:30:00.000+01:00" },
154
+ { value: 1.4143, start: "2026-01-11T13:45:00.000+01:00" },
155
+ { value: 1.448, start: "2026-01-11T14:00:00.000+01:00" },
156
+ { value: 1.448, start: "2026-01-11T14:15:00.000+01:00" },
157
+ { value: 1.448, start: "2026-01-11T14:30:00.000+01:00" },
158
+ { value: 1.448, start: "2026-01-11T14:45:00.000+01:00" },
159
+ { value: 1.4894, start: "2026-01-11T15:00:00.000+01:00" },
160
+ { value: 1.4894, start: "2026-01-11T15:15:00.000+01:00" },
161
+ { value: 1.4894, start: "2026-01-11T15:30:00.000+01:00" },
162
+ { value: 1.4894, start: "2026-01-11T15:45:00.000+01:00" },
163
+ { value: 1.6688, start: "2026-01-11T16:00:00.000+01:00" },
164
+ { value: 1.6688, start: "2026-01-11T16:15:00.000+01:00" },
165
+ { value: 1.6688, start: "2026-01-11T16:30:00.000+01:00" },
166
+ { value: 1.6688, start: "2026-01-11T16:45:00.000+01:00" },
167
+ { value: 1.7748, start: "2026-01-11T17:00:00.000+01:00" },
168
+ { value: 1.7748, start: "2026-01-11T17:15:00.000+01:00" },
169
+ { value: 1.7748, start: "2026-01-11T17:30:00.000+01:00" },
170
+ { value: 1.7748, start: "2026-01-11T17:45:00.000+01:00" },
171
+ { value: 1.7433, start: "2026-01-11T18:00:00.000+01:00" },
172
+ { value: 1.7433, start: "2026-01-11T18:15:00.000+01:00" },
173
+ { value: 1.7433, start: "2026-01-11T18:30:00.000+01:00" },
174
+ { value: 1.7433, start: "2026-01-11T18:45:00.000+01:00" },
175
+ { value: 1.8114, start: "2026-01-11T19:00:00.000+01:00" },
176
+ { value: 1.8114, start: "2026-01-11T19:15:00.000+01:00" },
177
+ { value: 1.8114, start: "2026-01-11T19:30:00.000+01:00" },
178
+ { value: 1.8114, start: "2026-01-11T19:45:00.000+01:00" },
179
+ { value: 1.5806, start: "2026-01-11T20:00:00.000+01:00" },
180
+ { value: 1.5806, start: "2026-01-11T20:15:00.000+01:00" },
181
+ { value: 1.5806, start: "2026-01-11T20:30:00.000+01:00" },
182
+ { value: 1.5806, start: "2026-01-11T20:45:00.000+01:00" },
183
+ { value: 1.5828, start: "2026-01-11T21:00:00.000+01:00" },
184
+ { value: 1.5828, start: "2026-01-11T21:15:00.000+01:00" },
185
+ { value: 1.5828, start: "2026-01-11T21:30:00.000+01:00" },
186
+ { value: 1.5828, start: "2026-01-11T21:45:00.000+01:00" },
187
+ { value: 1.3596, start: "2026-01-11T22:00:00.000+01:00" },
188
+ { value: 1.3596, start: "2026-01-11T22:15:00.000+01:00" },
189
+ { value: 1.3596, start: "2026-01-11T22:30:00.000+01:00" },
190
+ { value: 1.3596, start: "2026-01-11T22:45:00.000+01:00" },
191
+ { value: 1.2489, start: "2026-01-11T23:00:00.000+01:00" },
192
+ { value: 1.2489, start: "2026-01-11T23:15:00.000+01:00" },
193
+ { value: 1.2489, start: "2026-01-11T23:30:00.000+01:00" },
194
+ { value: 1.2489, start: "2026-01-11T23:45:00.000+01:00", end: "2026-01-12T00:00:00.000+01:00" },
195
+ ],
196
+ source: "Tibber",
197
+ };