UNPKG

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
"use strict"; 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 };