node-red-contrib-power-saver
Version:
A module for Node-RED that you can use to turn on and off a switch based on power prices
161 lines (151 loc) • 5.32 kB
JavaScript
;
const { fillArray } = require("./utils");
/**
* Takes an array of true/false values where true means on and false means off.
* Evaluates of the on/off sequences are valid according to other arguments.
*
* @param {*} onOff Array of on/off values
* @param {*} maxMinutesOff Max number of minutes that can be off in a sequence
* @param {*} minMinutesOff Min number of minutes that must be off to bother
* @param {*} recoveryPercentage Percent of off-time that must be on after being off
* @param {*} recoveryMaxMinutes Maximum recovery time in minutes
* @returns
*/
function isOnOffSequencesOk(onOff, maxMinutesOff, minMinutesOff, recoveryPercentage, recoveryMaxMinutes = null) {
let offCount = 0;
let onCount = 0;
let reachedMaxOff = false;
let reachedMinOn = true;
let reachedMinOff = null;
let minOnAfterOff = 0;
for (let i = 0; i < onOff.length; i++) {
if (!onOff[i]) {
if (maxMinutesOff === 0 || reachedMaxOff) {
return false;
}
if (!reachedMinOn) {
return false;
}
if (reachedMinOff === null) {
reachedMinOff = false;
}
offCount++;
onCount = 0;
if (offCount >= maxMinutesOff) {
reachedMaxOff = true;
}
if (offCount >= minMinutesOff) {
reachedMinOff = true;
}
const minRounded = Math.max(Math.round((offCount * recoveryPercentage) / 100), 1);
const recMaxMin = recoveryMaxMinutes === "" ? null : recoveryMaxMinutes;
minOnAfterOff = Math.min(minRounded, recMaxMin ?? minRounded);
if (i === onOff.length - 1) {
// If last minute, consider min reached
reachedMinOn = true;
reachedMinOff = true;
}
} else {
if (reachedMinOff === false) {
return false;
}
onCount++;
if (onCount >= minOnAfterOff) {
reachedMaxOff = false;
reachedMinOn = true;
} else {
reachedMinOn = false;
}
offCount = 0;
reachedMinOff = null;
}
}
return reachedMinOn && !(reachedMinOff === false);
}
/**
* Turn off the minutes where you save most compared to the next minute on.
*
* @param {*} values Array of prices
* @param {*} maxMinutesOff Max number of minutes that can be saved in a row
* @param {*} minMinutesOff Min number of minutes to turn off in a row
* @param {*} recoveryPercentage Min percent of time off that must be on after being off
* @param {*} recoveryMaxMinutes Maximum recovery time in minutes
* @param {*} minSaving Minimum amount that must be saved in order to turn off
* @param {*} lastValueDayBefore Value of the last minute the day before
* @param {*} lastCountDayBefore Number of lastValueDayBefore in a row
* @returns Array with same number of values as in values array, where true is on, false is off
*/
function calculate(
values,
maxMinutesOff,
minMinutesOff,
recoveryPercentage,
recoveryMaxMinutes,
minSaving,
lastValueDayBefore = undefined,
lastCountDayBefore = 0,
) {
const dayBefore = fillArray(lastValueDayBefore, lastCountDayBefore);
const last = values.length - 1;
// Create matrix with saving per minute
const savingPerMinute = [];
for (let minutes = 0; minutes < last; minutes++) {
const row = [];
for (let count = 1; count <= maxMinutesOff; count++) {
const on = minutes + count;
const saving = values[minutes] - values[on >= last ? last : on];
row.push(saving);
}
savingPerMinute.push(row);
}
// Create list with summary saving per sequence
let savingsList = [];
for (let minute = 0; minute < last; minute++) {
for (let count = 1; count <= maxMinutesOff; count++) {
let saving = 0;
for (let offset = 0; offset < count && minute + offset < last; offset++) {
saving += savingPerMinute[minute + offset][count - offset - 1];
}
if (saving > minSaving * count && minute + count <= last && values[minute] > values[minute + count] + minSaving) {
savingsList.push({ minute, count, saving });
}
}
}
savingsList.sort((b, a) => (b.saving === a.saving ? a.count - b.count : b.saving - a.saving));
let onOff = values.map(() => true); // Start with all on
// Find the best possible sequences
while (savingsList.length > 0) {
const { minute, count } = savingsList[savingsList.length - 1];
// Fast check: skip if any minute in this range is already turned off
let alreadyTaken = false;
for (let c = 0; c < count; c++) {
if (!onOff[minute + c]) {
alreadyTaken = true;
break;
}
}
if (alreadyTaken) {
savingsList.pop();
continue;
}
// Apply the off-period
for (let c = 0; c < count; c++) onOff[minute + c] = false;
if (
isOnOffSequencesOk(
dayBefore.length > 0 ? [...dayBefore, ...onOff] : onOff,
maxMinutesOff,
minMinutesOff,
recoveryPercentage,
recoveryMaxMinutes,
)
) {
savingsList = savingsList.filter((s) => s.minute < minute || s.minute >= minute + count);
} else {
// Roll back: only the minutes we changed (all were true before alreadyTaken check)
for (let c = 0; c < count; c++) onOff[minute + c] = true;
savingsList.pop();
}
}
return onOff;
}
module.exports = { calculate, isOnOffSequencesOk };