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
281 lines (253 loc) • 7.38 kB
JavaScript
const { DateTime } = require("luxon");
function booleanConfig(value) {
return value === "true" || value === true;
}
function calcNullSavings(values, _) {
return values.map(() => null);
}
/**
* Save the config object in the context, and set
* all values directly on the node.
*
* @param {*} node
* @param {*} originalConfig Object with config values
*/
function saveOriginalConfig(node, originalConfig) {
node.context().set("config", originalConfig);
}
/**
* Sort values in array and return array with index of original array
* in sorted order. Highest value first.
*/
function sortedIndex(valueArr) {
const mapped = valueArr.map((v, i) => {
return { i, value: v };
});
const sorted = mapped.sort((a, b) => {
if (a.value > b.value) {
return -1;
}
if (a.value < b.value) {
return 1;
}
return 0;
});
return sorted.map((p) => p.i);
}
/**
* Receive an array with values, and another array (same size) with
* true (on) and false (off) values.
* Returns an array with the difference between the current value
* and the next value that is on.
* If there is no next value that is on, the nextOn value is used.
* Positive difference is when the current value is higher than the next on.
*
* @param {*} values Array of prices
* @param {*} onOff Array of booleans, where true means on, false means off
* @param {*} nextOn Value for the next hour on after the whole values array,
* to use for the last hours.
* @returns Array with diff to next hour that is on. May be negative.
*
*/
function getDiffToNextOn(values, onOff, nextOn = null) {
const nextOnValue = nextOn ?? values[values.length - 1];
const res = values.map((p, i, a) => {
for (let n = i + 1; n < a.length; n++) {
if (onOff[n]) {
return getDiff(values[i], values[n]);
}
}
return getDiff(p, nextOnValue);
});
return res;
}
function getDiff(large, small) {
return roundPrice(large - small);
}
function getEffectiveConfig(node, msg) {
const res = node.context().get("config");
if (!res) {
node.error("Node has no config");
return {};
}
res.hasChanged = false;
const isConfigMsg = !!msg?.payload?.config;
if (isConfigMsg) {
const inputConfig = msg.payload.config;
Object.keys(inputConfig).forEach((key) => {
if (res[key] !== inputConfig[key]) {
res[key] = inputConfig[key];
res.hasChanged = true;
}
});
node.context().set("config", res);
}
// Store config variables in node
Object.keys(res).forEach((key) => (node[key] = res[key]));
return res;
}
function loadDayData(node, date) {
// Load saved schedule for the date (YYYY-MM-DD)
// Return null if not found
const key = date.toISODate();
const saved = node.context().get(key);
const res = saved ?? {
schedule: [],
hours: [],
};
return res;
}
function roundPrice(value) {
return Math.round(value * 10000) / 10000;
}
/**
*
* @param {*} values Array of prices
* @param {*} onOff Array of booleans, where true means on, false means off
* @param {*} nextOn Value for the next hour on after the whole values array,
* to use for the last hours.
* @returns Array with how much you save on the off-hours, null on the others.
*/
function getSavings(values, onOff, nextOn = null) {
return getDiffToNextOn(values, onOff, nextOn).map((v, i) => (onOff[i] ? null : v));
}
/**
* Takes an array of values and an array of true/valse values (same size).
* Returns the value from the first array
* corresponding to the first true value in the second array.
* If there is none, the defaultValue is returned.
*/
function firstOn(values, onOff, defaultValue = 0) {
return [...values, defaultValue][[...onOff, true].findIndex((e) => e)];
}
/**
* Count number of the given value at the end of the given array
* @param {*} arr
* @param {*} value
*/
function countAtEnd(arr, value) {
let res = 0;
for (let i = arr.length - 1; i >= 0; i--) {
if (arr[i] === value) {
res++;
} else {
break;
}
}
return res;
}
/**
* Return an array with an item for each time the value shall change.
* @param {*} onOff Array with on (true) and off (false) values.
* @param {*} startTimes Array with start time for each onOff value.
* @param {*} initial Optional. The initial value, to avoid the initial switch.
* @returns Array with tuples: time and value
*/
function makeSchedule(onOff, startTimes, initial = null) {
const res = [];
let prev = initial;
let prevRecord;
for (let i = 0; i < startTimes.length; i++) {
const value = onOff[i];
if (value !== prev || i === 0) {
const time = startTimes[i];
prevRecord = { time, value, countHours: 0 };
res.push(prevRecord);
prev = value;
}
prevRecord.countHours++;
}
return res;
}
function makeScheduleFromHours(hours, initial = null) {
return makeSchedule(
hours.map((h) => h.onOff),
hours.map((h) => h.start),
initial
);
}
function fillArray(value, count) {
if (value === undefined || count <= 0) {
return [];
}
res = [];
for (let i = 0; i < count; i++) {
res.push(value);
}
return res;
}
function extractPlanForDate(plan, day) {
const part = {};
part.hours = plan.hours.filter((h) => isSameDate(day, h.start));
part.schedule = plan.schedule.filter((s) => isSameDate(day, s.time));
return part;
}
function isSameDate(date1, date2) {
return DateTime.fromISO(date1).toISODate() === DateTime.fromISO(date2).toISODate();
}
function getStartAtIndex(effectiveConfig, priceData, time) {
if (effectiveConfig.scheduleOnlyFromCurrentTime) {
return priceData.map((p) => DateTime.fromISO(p.start)).filter((t) => t < time).length;
} else {
return 0;
}
}
function validationFailure(node, message, status = null) {
node.status({ fill: "red", shape: "ring", text: status ?? message });
node.warn(message);
}
function msgHasPriceData(msg) {
return !!msg?.payload?.priceData;
}
function msgHasConfig(msg) {
return !!msg?.payload?.config;
}
function fixOutputValues(config) {
if (config.outputValueForOntype === "bool") {
config.outputValueForOn = booleanConfig(config.outputValueForOn);
}
if (config.outputValueForOntype === "num") {
config.outputValueForOn = Number(config.outputValueForOn);
}
if (config.outputValueForOfftype === "bool") {
config.outputValueForOff = booleanConfig(config.outputValueForOff);
}
if (config.outputValueForOfftype === "num") {
config.outputValueForOff = Number(config.outputValueForOff);
}
}
function fixPeriods(config) {
config.periods.forEach((p) => {
p.value = p.value === "true" || p.value === true;
});
}
function getOutputForTime(schedule, time, defaultValue) {
const pastSchedule = schedule.filter((entry) => DateTime.fromISO(entry.time) <= time);
return pastSchedule.length ? pastSchedule[pastSchedule.length - 1].value : defaultValue;
}
module.exports = {
booleanConfig,
calcNullSavings,
countAtEnd,
extractPlanForDate,
fillArray,
firstOn,
fixOutputValues,
fixPeriods,
getDiff,
getDiffToNextOn,
getEffectiveConfig,
getOutputForTime,
getSavings,
getStartAtIndex,
isSameDate,
loadDayData,
makeSchedule,
makeScheduleFromHours,
msgHasConfig,
msgHasPriceData,
roundPrice,
saveOriginalConfig,
sortedIndex,
validationFailure,
};