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

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.
@@ -7,6 +7,15 @@ sidebarDepth: 1
7
7
 
8
8
  List the most significant changes.
9
9
 
10
+ ## 5.0.0.beta.4
11
+
12
+ - Fix bug on recovery time when recoveryMaxMinutes is not set
13
+
14
+ ## 5.0.0.beta.3
15
+
16
+ - Implement minute support in Heat Capacitor
17
+ - Fix bug so recoveryPercentage is used properly
18
+
10
19
  ## 5.0.0.beta.2
11
20
 
12
21
  - Fix bug in Min minutes off
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-saver",
3
- "version": "5.0.0-beta.2",
3
+ "version": "5.0.0-beta.4",
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": {
@@ -45,7 +45,8 @@ function isOnOffSequencesOk(
45
45
  reachedMinOff = true;
46
46
  }
47
47
  const minRounded = Math.max(Math.round(offCount * recoveryPercentage / 100), 1)
48
- minOnAfterOff = Math.min(minRounded, recoveryMaxMinutes ?? minRounded)
48
+ const recMaxMin = recoveryMaxMinutes === "" ? null : recoveryMaxMinutes;
49
+ minOnAfterOff = Math.min(minRounded, recMaxMin ?? minRounded)
49
50
  if(i === onOff.length - 1) {
50
51
  // If last minute, consider min reached
51
52
  reachedMinOn = true;
@@ -59,6 +60,8 @@ function isOnOffSequencesOk(
59
60
  if (onCount >= minOnAfterOff) {
60
61
  reachedMaxOff = false;
61
62
  reachedMinOn = true;
63
+ } else {
64
+ reachedMinOn = false;
62
65
  }
63
66
  offCount = 0;
64
67
  reachedMinOff = null;
@@ -2,22 +2,51 @@
2
2
  const { DateTime } = require("luxon");
3
3
  const { roundPrice, getDiffToNextOn } = require("./utils");
4
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;
5
+ function buildMinutePriceVector(priceData) {
6
+ if (!priceData || priceData.length === 0) {
7
+ return { minutePrices: [], startDate: null };
11
8
  }
12
9
 
10
+ const sorted = [...priceData].sort(
11
+ (a, b) => DateTime.fromISO(a.start).toMillis() - DateTime.fromISO(b.start).toMillis()
12
+ );
13
+
14
+ const minutePrices = [];
15
+ let previousIntervalMinutes = 60;
16
+
17
+ for (let i = 0; i < sorted.length; i++) {
18
+ const currentStart = DateTime.fromISO(sorted[i].start);
19
+ let intervalMinutes = previousIntervalMinutes;
20
+
21
+ if (sorted[i + 1]) {
22
+ intervalMinutes = DateTime.fromISO(sorted[i + 1].start).diff(currentStart, "minutes").minutes;
23
+ } else if (sorted[i].end) {
24
+ intervalMinutes = DateTime.fromISO(sorted[i].end).diff(currentStart, "minutes").minutes;
25
+ }
26
+
27
+ intervalMinutes = Math.max(1, Math.round(intervalMinutes));
28
+ previousIntervalMinutes = intervalMinutes;
29
+
30
+ for (let m = 0; m < intervalMinutes; m++) {
31
+ minutePrices.push(sorted[i].value);
32
+ }
33
+ }
34
+
35
+ return { minutePrices, startDate: DateTime.fromISO(sorted[0].start) };
36
+ }
37
+
38
+ function calculateOpportunities(pricesPerMinute, pattern, amount) {
39
+ const tempPrice = pricesPerMinute;
40
+
13
41
  //Calculate weighted pattern
14
42
  const weight = amount / pattern.reduce((a, b) => a + b, 0); //last calculates the sum of all numbers in the pattern
15
43
  const weightedPattern = pattern.map((x) => x * weight);
16
44
 
17
45
  //Calculating procurement opportunities. Sliding the pattern over the price vector to find the price for procuring
18
46
  //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);
47
+ const dot = (a, b) => a.map((x, i) => a[i] * b[i]).reduce((m, n) => m + n, 0);
48
+ const procurementLength = Math.max(tempPrice.length - pattern.length + 1, 0);
49
+ const procurementOpportunities = Array(procurementLength);
21
50
  for (let i = 0; i < procurementOpportunities.length; i++) {
22
51
  procurementOpportunities[i] = dot(weightedPattern, tempPrice.slice(i, i + pattern.length));
23
52
  }
@@ -232,8 +261,7 @@ function runBuySellAlgorithm(
232
261
  maxTempAdjustment,
233
262
  minSavings
234
263
  ) {
235
- const prices = [...priceData.map((pd) => pd.value)];
236
- const startDate = DateTime.fromISO(priceData[0].start);
264
+ const { minutePrices, startDate } = buildMinutePriceVector(priceData);
237
265
 
238
266
  //pattern for how much power is procured/sold when.
239
267
  //This has, for now, just a flat acquisition/divestment profile
@@ -243,8 +271,8 @@ function runBuySellAlgorithm(
243
271
  const sellPattern = Array(sellDuration).fill(1);
244
272
 
245
273
  //Calculate what it will cost to procure/sell 1 kWh as a function of time
246
- const buyPrices = calculateOpportunities(prices, buyPattern, 1);
247
- const sellPrices = calculateOpportunities(prices, sellPattern, 1);
274
+ const buyPrices = calculateOpportunities(minutePrices, buyPattern, 1);
275
+ const sellPrices = calculateOpportunities(minutePrices, sellPattern, 1);
248
276
 
249
277
  //Find dates for when to procure/sell
250
278
  const buySell = findBestBuySellPattern(buyPrices, buyPattern.length, sellPrices, sellPattern.length);
@@ -80,10 +80,14 @@ module.exports = function (RED) {
80
80
  if (msg.payload.hasOwnProperty("priceData")) {
81
81
  if (node.hasOwnProperty("priceData")) {
82
82
  node.priceData = mergePriceData(node.priceData, msg.payload.priceData);
83
- if (node.priceData.length > 72) node.priceData = node.priceData.slice(-72);
84
83
  } else {
85
84
  node.priceData = msg.payload.priceData;
86
85
  }
86
+ if (node.priceData.length) {
87
+ const latestStart = DateTime.fromISO(node.priceData[node.priceData.length - 1].start);
88
+ const cutoff = latestStart.minus({ hours: 72 });
89
+ node.priceData = node.priceData.filter((entry) => DateTime.fromISO(entry.start) >= cutoff);
90
+ }
87
91
  }
88
92
 
89
93
  if (node.hasOwnProperty("priceData")) {
@@ -136,21 +140,16 @@ module.exports = function (RED) {
136
140
  };
137
141
 
138
142
  function mergePriceData(priceDataA, priceDataB) {
139
- const tempDict = {};
140
- priceDataA.forEach((e) => {
141
- tempDict[e.start] = e.value;
142
- });
143
- priceDataB.forEach((e) => {
144
- tempDict[e.start] = e.value;
143
+ const mergedEntries = new Map();
144
+ priceDataA.concat(priceDataB).forEach((entry) => {
145
+ if (!entry?.start) {
146
+ return;
147
+ }
148
+ const existing = mergedEntries.get(entry.start);
149
+ mergedEntries.set(entry.start, Object.assign({}, existing, entry));
145
150
  });
146
151
 
147
- const keys = Object.keys(tempDict);
148
- keys.sort();
149
-
150
- const res = Array(keys.length);
151
- for (let i = 0; i < res.length; i++) {
152
- res[i] = { value: tempDict[keys[i]], start: keys[i] };
153
- }
154
-
155
- return res;
152
+ return Array.from(mergedEntries.values()).sort(
153
+ (a, b) => DateTime.fromISO(a.start).toMillis() - DateTime.fromISO(b.start).toMillis()
154
+ );
156
155
  }