node-red-contrib-power-saver 1.0.7 → 2.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Change Log
2
+
3
+ List the most significant changes, starting in version 1.0.9.
4
+
5
+ ## 2.0.1
6
+
7
+ * Fix bug that caused no schedule
8
+ * Add config to output
9
+
10
+ ## 2.0.0
11
+
12
+ * New and better algorithm to calculates savings, resulting in a better schedule.
13
+ * Removed possibility to configure maximum hours to save per day, as this does not really make much sense.
14
+ * Round savings to 4 decimals.
15
+ * Set last savings hour to null when 0.
16
+
17
+ ## 1.0.9
18
+
19
+ * Fix bug in saving last hour of the day.
20
+
21
+
package/README.md CHANGED
@@ -155,3 +155,7 @@ Are you using [MagicMirror](https://magicmirror.builders/)? Are you also using [
155
155
  The purple lines show savings.
156
156
 
157
157
  Read more about this in the [MMM-Tibber documentation](https://github.com/ottopaulsen/MMM-Tibber#show-savings).
158
+
159
+ ## Change Log
160
+
161
+ [Change Log](CHANGELOG.md)
@@ -1,24 +1,13 @@
1
- const {
2
- sortedIndex,
3
- getDiffToNextOn,
4
- isOnOffSequencesOk,
5
- fillArray,
6
- } = require("./utils");
1
+ "use strict";
2
+
3
+ const { isOnOffSequencesOk, fillArray } = require("./utils");
7
4
  /**
8
5
  * Turn off the hours where you save most compared to the next hour on.
9
6
  *
10
- * Algorithm:
11
- * 1. For each hour, find out how much is saved if turned off.
12
- * Compare price with the net hour that is on.
13
- * 2. Turn off the hour that saves the most.
14
- * 3. Validate. Keep if valid. If not, turn off the next best. And so on.
15
- * When validating, include hours from day before, but do not fail on day before.
16
- * 4. If something was turned off, repeat from 1.
17
- *
18
7
  * @param {*} values Array of prices
19
- * @param {*} maxOffCount Max number of hours that can be saved in total
20
8
  * @param {*} maxOffInARow Max number of hours that can be saved in a row
21
9
  * @param {*} minOnAfterMaxOffInARow Min number of hours that must be on after maxOffInARow is saved
10
+ * @param {*} minSaving Minimum amount that must be saved in order to turn off
22
11
  * @param {*} lastValueDayBefore Value of the last hour the day before
23
12
  * @param {*} lastCountDayBefore Number of lastValueDayBefore in a row
24
13
  * @returns Array with same number of values as in values array, where true is on, false is off
@@ -26,7 +15,6 @@ const {
26
15
  module.exports = {
27
16
  calculate: function (
28
17
  values,
29
- maxOffCount,
30
18
  maxOffInARow,
31
19
  minOnAfterMaxOffInARow,
32
20
  minSaving,
@@ -34,35 +22,63 @@ module.exports = {
34
22
  lastCountDayBefore = 0
35
23
  ) {
36
24
  const dayBefore = fillArray(lastValueDayBefore, lastCountDayBefore);
37
- let foundImprovement;
38
- const onOff = values.map((v) => true); // Start with all on
39
- if (maxOffCount <= 0) {
40
- return onOff;
25
+ const last = values.length - 1;
26
+
27
+ // Create matrix with saving per hour
28
+ const savingPerHour = [];
29
+ for (let hour = 0; hour < last; hour++) {
30
+ const row = [];
31
+ for (let count = 1; count <= maxOffInARow; count++) {
32
+ const on = hour + count;
33
+ const saving = values[hour] - values[on >= last ? last : on];
34
+ row.push(saving);
35
+ }
36
+ savingPerHour.push(row);
41
37
  }
42
- do {
43
- foundImprovement = false;
44
- const diffToNextOn = getDiffToNextOn(values, onOff);
45
- const sorted = sortedIndex(diffToNextOn).filter(
46
- (v) => onOff[v] && diffToNextOn[v] >= minSaving
47
- );
48
- let tryToTurnOffIndex = 0;
49
- while (tryToTurnOffIndex < sorted.length && !foundImprovement) {
50
- onOff[sorted[tryToTurnOffIndex]] = false;
38
+
39
+ // Create list with summary saving per sequence
40
+ let savingsList = [];
41
+ for (let hour = 0; hour < last; hour++) {
42
+ for (let count = 1; count <= maxOffInARow; count++) {
43
+ let saving = 0;
44
+ for (let offset = 0; offset < count && hour + offset < last; offset++) {
45
+ saving += savingPerHour[hour + offset][count - offset - 1];
46
+ }
51
47
  if (
52
- isOnOffSequencesOk(
53
- [...dayBefore, ...onOff],
54
- maxOffInARow,
55
- minOnAfterMaxOffInARow
56
- )
48
+ saving > minSaving * count &&
49
+ hour + count <= last &&
50
+ values[hour] > values[hour + count] + minSaving
57
51
  ) {
58
- foundImprovement = true;
59
- } else {
60
- onOff[sorted[tryToTurnOffIndex]] = true;
61
- tryToTurnOffIndex++;
52
+ savingsList.push({ hour, count, saving });
62
53
  }
63
54
  }
64
- } while (foundImprovement && onOff.filter((v) => !v).length < maxOffCount);
55
+ }
56
+
57
+ savingsList.sort((a, b) => b.saving - a.saving);
58
+ let onOff = values.map((v) => true); // Start with all on
65
59
 
60
+ // Find the best possible sequences
61
+ while (savingsList.length > 0) {
62
+ const { hour, count } = savingsList[0];
63
+ const onOffCopy = [...onOff];
64
+ for (let c = 0; c < count; c++) {
65
+ onOff[hour + c] = false;
66
+ }
67
+ if (
68
+ isOnOffSequencesOk(
69
+ [...dayBefore, ...onOff],
70
+ maxOffInARow,
71
+ minOnAfterMaxOffInARow
72
+ )
73
+ ) {
74
+ savingsList = savingsList.filter(
75
+ (s) => s.hour < hour || s.hour >= hour + count
76
+ );
77
+ } else {
78
+ onOff = [...onOffCopy];
79
+ savingsList.splice(0, 1);
80
+ }
81
+ }
66
82
  return onOff;
67
83
  },
68
84
  };
@@ -0,0 +1,68 @@
1
+ const {
2
+ sortedIndex,
3
+ getDiffToNextOn,
4
+ isOnOffSequencesOk,
5
+ fillArray,
6
+ } = require("./utils");
7
+ /**
8
+ * Turn off the hours where you save most compared to the next hour on.
9
+ *
10
+ * Algorithm:
11
+ * 1. For each hour, find out how much is saved if turned off.
12
+ * Compare price with the net hour that is on.
13
+ * 2. Turn off the hour that saves the most.
14
+ * 3. Validate. Keep if valid. If not, turn off the next best. And so on.
15
+ * When validating, include hours from day before, but do not fail on day before.
16
+ * 4. If something was turned off, repeat from 1.
17
+ *
18
+ * @param {*} values Array of prices
19
+ * @param {*} maxOffInARow Max number of hours that can be saved in a row
20
+ * @param {*} minOnAfterMaxOffInARow Min number of hours that must be on after maxOffInARow is saved
21
+ * @param {*} minSaving Minimum amount that must be saved in order to turn off
22
+ * @param {*} lastValueDayBefore Value of the last hour the day before
23
+ * @param {*} lastCountDayBefore Number of lastValueDayBefore in a row
24
+ * @returns Array with same number of values as in values array, where true is on, false is off
25
+ */
26
+ module.exports = {
27
+ calculate: function (
28
+ values,
29
+ maxOffInARow,
30
+ minOnAfterMaxOffInARow,
31
+ minSaving,
32
+ lastValueDayBefore = undefined,
33
+ lastCountDayBefore = 0
34
+ ) {
35
+ const dayBefore = fillArray(lastValueDayBefore, lastCountDayBefore);
36
+ let foundImprovement;
37
+ const onOff = values.map((v) => true); // Start with all on
38
+ do {
39
+ foundImprovement = false;
40
+ const diffToNextOn = getDiffToNextOn(
41
+ values,
42
+ onOff,
43
+ values[values.length - 1]
44
+ );
45
+ const sorted = sortedIndex(diffToNextOn).filter(
46
+ (v) => onOff[v] && diffToNextOn[v] >= minSaving
47
+ );
48
+ let tryToTurnOffIndex = 0;
49
+ while (tryToTurnOffIndex < sorted.length && !foundImprovement) {
50
+ onOff[sorted[tryToTurnOffIndex]] = false;
51
+ if (
52
+ isOnOffSequencesOk(
53
+ [...dayBefore, ...onOff],
54
+ maxOffInARow,
55
+ minOnAfterMaxOffInARow
56
+ )
57
+ ) {
58
+ foundImprovement = true;
59
+ } else {
60
+ onOff[sorted[tryToTurnOffIndex]] = true;
61
+ tryToTurnOffIndex++;
62
+ }
63
+ }
64
+ } while (foundImprovement);
65
+
66
+ return onOff;
67
+ },
68
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-saver",
3
- "version": "1.0.7",
3
+ "version": "2.0.1",
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": {
@@ -9,7 +9,12 @@
9
9
  "author": "Otto Paulsen <ottpau@gmail.com>",
10
10
  "license": "MIT",
11
11
  "keywords": [
12
- "node-red"
12
+ "node-red",
13
+ "tibber",
14
+ "energy",
15
+ "smarthome",
16
+ "home-automation",
17
+ "power"
13
18
  ],
14
19
  "node-red": {
15
20
  "nodes": {
package/power-saver.html CHANGED
@@ -4,11 +4,6 @@
4
4
  color: "#a6bbcf",
5
5
  defaults: {
6
6
  name: { value: "Power Saver" },
7
- maxHoursToSavePerDay: {
8
- value: 12,
9
- required: true,
10
- validate: RED.validators.number(),
11
- },
12
7
  maxHoursToSaveInSequence: {
13
8
  value: 3,
14
9
  required: true,
@@ -47,10 +42,6 @@
47
42
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
48
43
  <input type="text" id="node-input-name" placeholder="Name">
49
44
  </div>
50
- <div class="form-row">
51
- <label for="node-input-maxHoursToSavePerDay"><i class="fa fa-align-justify"></i> Max per day</label>
52
- <input type="text" id="node-input-maxHoursToSavePerDay" placeholder="Max hours to save in a day">
53
- </div>
54
45
  <div class="form-row">
55
46
  <label for="node-input-maxHoursToSaveInSequence"><i class="fa fa-arrows-h"></i> Max per sequence</label>
56
47
  <input type="text" id="node-input-maxHoursToSaveInSequence" placeholder="Max hours to save in sequence">
@@ -80,9 +71,6 @@
80
71
 
81
72
 
82
73
  <div>
83
- <p>
84
- <strong>Max per day</strong> is the maximum number of hours that can be saved (turned off) during a day.
85
- </p>
86
74
  <p>
87
75
  <strong>Max per sequence</strong> is the maximum number of hours that can be saved (turned off) in a sequence.
88
76
  </p>
package/power-saver.js CHANGED
@@ -1,5 +1,11 @@
1
1
  const { DateTime } = require("luxon");
2
- const { convertMsg, countAtEnd, makeSchedule, getSavings } = require("./utils");
2
+ const {
3
+ convertMsg,
4
+ countAtEnd,
5
+ makeSchedule,
6
+ getSavings,
7
+ extractPlanForDate,
8
+ } = require("./utils");
3
9
  const mostSavedStrategy = require("./mostSavedStrategy");
4
10
 
5
11
  let schedulingTimeout = null;
@@ -10,11 +16,10 @@ module.exports = function (RED) {
10
16
  const node = this;
11
17
 
12
18
  // Save config in node
13
- this.maxHoursToSavePerDay = config.maxHoursToSavePerDay;
14
19
  this.maxHoursToSaveInSequence = config.maxHoursToSaveInSequence;
15
20
  this.minHoursOnAfterMaxSequenceSaved =
16
21
  config.minHoursOnAfterMaxSequenceSaved;
17
- this.minSaving = config.minSaving;
22
+ this.minSaving = parseFloat(config.minSaving);
18
23
  this.sendCurrentValueWhenRescheduling =
19
24
  config.sendCurrentValueWhenRescheduling;
20
25
  this.outputIfNoSchedule = config.outputIfNoSchedule === "true";
@@ -33,55 +38,43 @@ module.exports = function (RED) {
33
38
  return;
34
39
  }
35
40
 
36
- const today = input.today;
37
- const tomorrow = input.tomorrow;
38
- const source = input.source;
41
+ const priceData = [...input.today, ...input.tomorrow];
39
42
 
40
43
  clearTimeout(schedulingTimeout);
41
44
 
42
- // Set dates
43
- const todaysDate = DateTime.fromISO(today[0].start.substr(0, 10));
44
- const yesterdayDate = todaysDate.plus({ days: -1 });
45
- const tomorrowDate = todaysDate.plus({ days: 1 });
45
+ const dates = [
46
+ ...new Set(priceData.map((v) => DateTime.fromISO(v.start).toISODate())),
47
+ ];
46
48
 
47
- // Load data from yesterday
48
- const dataYesterday = loadDayData(node, yesterdayDate);
49
+ // Load data from day before
50
+ const dateDayBefore = DateTime.fromISO(dates[0]).plus({ days: -1 });
51
+ const dataDayBefore = loadDayData(node, dateDayBefore);
49
52
 
50
53
  // Make plan
51
- const valuesToday = today.map((d) => d.value);
52
- const valuesTomorrow = tomorrow.map((d) => d.value);
53
- const startTimesToday = today.map((d) => d.start);
54
- const startTimesTomorrow = tomorrow.map((d) => d.start);
55
-
56
- const planToday = makePlan(
57
- node,
58
- valuesToday,
59
- startTimesToday,
60
- dataYesterday.onOff
61
- );
62
- const planTomorrow = makePlan(
63
- node,
64
- valuesTomorrow,
65
- startTimesTomorrow,
66
- planToday.onOff
67
- );
54
+ const values = priceData.map((d) => d.value);
55
+ const startTimes = priceData.map((d) => d.start);
56
+ const plan = makePlan(node, values, startTimes, dataDayBefore.onOff);
68
57
 
69
58
  // Save schedule
70
- saveDayData(node, todaysDate, planToday);
71
- saveDayData(node, tomorrowDate, planTomorrow);
72
-
73
- // Combine data for today and tomorrow
74
- const schedule = [...planToday.schedule, ...planTomorrow.schedule];
75
- const hours = [...planToday.hours, ...planTomorrow.hours];
59
+ dates.forEach((d) => saveDayData(node, d, extractPlanForDate(plan, d)));
60
+
61
+ const config = {
62
+ maxHoursToSaveInSequence: this.maxHoursToSaveInSequence,
63
+ minHoursOnAfterMaxSequenceSaved: this.minHoursOnAfterMaxSequenceSaved,
64
+ minSaving: this.minSaving,
65
+ sendCurrentValueWhenRescheduling: this.sendCurrentValueWhenRescheduling,
66
+ outputIfNoSchedule: this.outputIfNoSchedule,
67
+ };
76
68
 
77
69
  // Prepare output
78
70
  let output1 = null;
79
71
  let output2 = null;
80
72
  let output3 = {
81
73
  payload: {
82
- schedule,
83
- hours,
84
- source,
74
+ schedule: plan.schedule,
75
+ hours: plan.hours,
76
+ source: input.source,
77
+ config,
85
78
  },
86
79
  };
87
80
 
@@ -89,7 +82,7 @@ module.exports = function (RED) {
89
82
  const time = msg.payload.time
90
83
  ? DateTime.fromISO(msg.payload.time)
91
84
  : DateTime.now();
92
- const pastSchedule = schedule.filter(
85
+ const pastSchedule = plan.schedule.filter(
93
86
  (entry) => DateTime.fromISO(entry.time) <= time
94
87
  );
95
88
  const outputCurrent = node.sendCurrentValueWhenRescheduling;
@@ -101,13 +94,13 @@ module.exports = function (RED) {
101
94
  }
102
95
 
103
96
  // Delete old data
104
- deleteSavedScheduleBefore(node, yesterdayDate);
97
+ deleteSavedScheduleBefore(node, dateDayBefore);
105
98
 
106
99
  // Send output
107
100
  node.send([output1, output2, output3]);
108
101
 
109
102
  // Run schedule
110
- schedulingTimeout = runSchedule(node, schedule, time);
103
+ schedulingTimeout = runSchedule(node, plan.schedule, time);
111
104
  });
112
105
  }
113
106
 
@@ -130,8 +123,7 @@ function loadDayData(node, date) {
130
123
  }
131
124
 
132
125
  function saveDayData(node, date, plan) {
133
- const key = date.toISO();
134
- node.context().set(key, plan);
126
+ node.context().set(date, plan);
135
127
  }
136
128
 
137
129
  function deleteSavedScheduleBefore(node, day) {
@@ -142,7 +134,7 @@ function deleteSavedScheduleBefore(node, day) {
142
134
  } while (data);
143
135
  }
144
136
 
145
- function makePlan(node, values, startTimes, onOffBefore) {
137
+ function makePlan(node, values, startTimes, onOffBefore, firstValueNextDay) {
146
138
  const strategy = "mostSaved"; // TODO: Get from node settings
147
139
  const lastValueDayBefore = onOffBefore[onOffBefore.length - 1];
148
140
  const lastCountDayBefore = countAtEnd(onOffBefore, lastValueDayBefore);
@@ -150,7 +142,6 @@ function makePlan(node, values, startTimes, onOffBefore) {
150
142
  strategy === "mostSaved"
151
143
  ? mostSavedStrategy.calculate(
152
144
  values,
153
- node.maxHoursToSavePerDay,
154
145
  node.maxHoursToSaveInSequence,
155
146
  node.minHoursOnAfterMaxSequenceSaved,
156
147
  node.minSaving,
@@ -160,7 +151,7 @@ function makePlan(node, values, startTimes, onOffBefore) {
160
151
  : [];
161
152
 
162
153
  const schedule = makeSchedule(onOff, startTimes, lastValueDayBefore);
163
- const savings = getSavings(values, onOff);
154
+ const savings = getSavings(values, onOff, firstValueNextDay);
164
155
  const hours = values.map((v, i) => ({
165
156
  price: v,
166
157
  onOff: onOff[i],
@@ -0,0 +1,169 @@
1
+ module.exports = {
2
+ schedule: [
3
+ {
4
+ time: "2021-06-20T01:50:00.000+02:00",
5
+ value: true,
6
+ },
7
+ {
8
+ time: "2021-06-20T01:50:00.030+02:00",
9
+ value: false,
10
+ },
11
+ {
12
+ time: "2021-06-20T01:50:00.050+02:00",
13
+ value: true,
14
+ },
15
+ {
16
+ time: "2021-06-20T01:50:00.060+02:00",
17
+ value: false,
18
+ },
19
+ {
20
+ time: "2021-06-20T01:50:00.080+02:00",
21
+ value: true,
22
+ },
23
+ {
24
+ time: "2021-06-20T01:50:00.090+02:00",
25
+ value: false,
26
+ },
27
+ {
28
+ time: "2021-06-20T01:50:00.120+02:00",
29
+ value: true,
30
+ },
31
+ {
32
+ time: "2021-06-20T01:50:00.150+02:00",
33
+ value: false,
34
+ },
35
+ {
36
+ time: "2021-06-20T01:50:00.180+02:00",
37
+ value: true,
38
+ },
39
+ ],
40
+ hours: [
41
+ {
42
+ price: 0.3,
43
+ onOff: true,
44
+ start: "2021-06-20T01:50:00.000+02:00",
45
+ saving: null,
46
+ },
47
+ {
48
+ price: 0.4,
49
+ onOff: true,
50
+ start: "2021-06-20T01:50:00.010+02:00",
51
+ saving: null,
52
+ },
53
+ {
54
+ price: 0.8,
55
+ onOff: true,
56
+ start: "2021-06-20T01:50:00.020+02:00",
57
+ saving: null,
58
+ },
59
+ {
60
+ price: 0.9,
61
+ onOff: false,
62
+ start: "2021-06-20T01:50:00.030+02:00",
63
+ saving: 0.3,
64
+ },
65
+ {
66
+ price: 0.7,
67
+ onOff: false,
68
+ start: "2021-06-20T01:50:00.040+02:00",
69
+ saving: 0.1,
70
+ },
71
+ {
72
+ price: 0.6,
73
+ onOff: true,
74
+ start: "2021-06-20T01:50:00.050+02:00",
75
+ saving: null,
76
+ },
77
+ {
78
+ price: 0.5,
79
+ onOff: false,
80
+ start: "2021-06-20T01:50:00.060+02:00",
81
+ saving: 0.3,
82
+ },
83
+ {
84
+ price: 0.75,
85
+ onOff: false,
86
+ start: "2021-06-20T01:50:00.070+02:00",
87
+ saving: 0.55,
88
+ },
89
+ {
90
+ price: 0.2,
91
+ onOff: true,
92
+ start: "2021-06-20T01:50:00.080+02:00",
93
+ saving: null,
94
+ },
95
+ {
96
+ price: 0.85,
97
+ onOff: false,
98
+ start: "2021-06-20T01:50:00.090+02:00",
99
+ saving: 0.05,
100
+ },
101
+ {
102
+ price: 1.5,
103
+ onOff: false,
104
+ start: "2021-06-20T01:50:00.100+02:00",
105
+ saving: 0.7,
106
+ },
107
+ {
108
+ price: 1.4,
109
+ onOff: false,
110
+ start: "2021-06-20T01:50:00.110+02:00",
111
+ saving: 0.6,
112
+ },
113
+ {
114
+ price: 0.8,
115
+ onOff: true,
116
+ start: "2021-06-20T01:50:00.120+02:00",
117
+ saving: null,
118
+ },
119
+ {
120
+ price: 0.9,
121
+ onOff: true,
122
+ start: "2021-06-20T01:50:00.130+02:00",
123
+ saving: null,
124
+ },
125
+ {
126
+ price: 0.7,
127
+ onOff: true,
128
+ start: "2021-06-20T01:50:00.140+02:00",
129
+ saving: null,
130
+ },
131
+ {
132
+ price: 0.6,
133
+ onOff: false,
134
+ start: "2021-06-20T01:50:00.150+02:00",
135
+ saving: 0.4,
136
+ },
137
+ {
138
+ price: 0.5,
139
+ onOff: false,
140
+ start: "2021-06-20T01:50:00.160+02:00",
141
+ saving: 0.3,
142
+ },
143
+ {
144
+ price: 0.75,
145
+ onOff: false,
146
+ start: "2021-06-20T01:50:00.170+02:00",
147
+ saving: 0.55,
148
+ },
149
+ {
150
+ price: 0.2,
151
+ onOff: true,
152
+ start: "2021-06-20T01:50:00.180+02:00",
153
+ saving: null,
154
+ },
155
+ {
156
+ price: 0.85,
157
+ onOff: true,
158
+ start: "2021-06-20T01:50:00.190+02:00",
159
+ saving: null,
160
+ },
161
+ ],
162
+ source: "Other",
163
+ config: {
164
+ maxHoursToSaveInSequence: 3,
165
+ minHoursOnAfterMaxSequenceSaved: 2,
166
+ minSaving: 0.001,
167
+ outputIfNoSchedule: false,
168
+ },
169
+ };