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
199 lines (170 loc) • 5.83 kB
JavaScript
const { extractPlanForDate, loadDayData, makeSchedule, msgHasPriceData, validationFailure } = require("./utils");
const { DateTime } = require("luxon");
function handleStrategyInput(node, msg, config, doPlanning, calcSavings) {
if (!validateInput(node, msg)) {
return;
}
const commands = getCommands(msg);
if (commands.reset) {
node.warn("Resetting node context by command");
// Reset all saved data
node
.context()
.set(["lastPlan", "lastPriceData", "lastSource"], [undefined, undefined, undefined], node.contextStorage);
deleteSavedScheduleBefore(node, DateTime.now().plus({ days: 2 }), 100);
}
const plan =
msgHasPriceData(msg) || config.hasChanged
? makePlanFromPriceData(node, msg, config, doPlanning, calcSavings)
: node.context().get("lastPlan", node.contextStorage);
// If still no plan?
if (!plan) {
const message = "No price data";
node.warn(message);
node.status({ fill: "yellow", shape: "dot", text: message });
return;
}
return { plan, commands };
}
function makePlanFromPriceData(node, msg, config, doPlanning, calcSavings) {
const { priceData, source } = msgHasPriceData(msg) ? getPriceDataFromMessage(msg) : getSavedLastPriceData(node);
if (msgHasPriceData(msg)) {
saveLastPriceData(node, priceData, source);
}
if (!priceData) {
return null;
}
const dates = [...new Set(priceData.map((v) => DateTime.fromISO(v.start).toISODate()))];
// Load data from day before
const dateDayBefore = DateTime.fromISO(dates[0]).plus({ days: -1 });
const dataDayBefore = loadDataJustBefore(node, dateDayBefore);
const priceDataDayBefore = dataDayBefore.hours.map((h) => ({ value: h.price, start: h.start }));
const priceDataWithDayBefore = [...priceDataDayBefore, ...priceData];
// Make plan
const startTimes = priceDataWithDayBefore.map((d) => d.start);
const prices = priceDataWithDayBefore.map((d) => d.value);
const onOff = doPlanning(node, priceDataWithDayBefore);
const savings = calcSavings(prices, onOff);
const hours = startTimes.map((v, i) => ({
start: startTimes[i],
price: prices[i],
onOff: onOff[i],
saving: savings[i],
}));
const schedule = makeSchedule(onOff, startTimes);
addLastSwitchIfNoSchedule(schedule, hours, config);
plan = {
hours,
schedule,
source,
};
// Save schedule
node.context().set("lastPlan", plan, node.contextStorage);
dates.forEach((d) => saveDayData(node, d, extractPlanForDate(plan, d)));
// Delete old data
deleteSavedScheduleBefore(node, dateDayBefore);
return plan;
}
// Commands
function getCommands(msg) {
const legalCommands = ["reset", "replan", "sendOutput", "sendSchedule"];
const commands = { legal: true };
if (msg.payload?.config?.override === "auto") {
commands.runSchedule = true;
}
if (!msg?.payload?.commands) {
return commands;
}
legalCommands.forEach((c) => {
commands[c] = msg.payload.commands[c];
});
return commands;
}
// Price data
function getPriceDataFromMessage(msg) {
const priceData = msg.payload.priceData;
const source = msg.payload.source;
return { priceData, source };
}
function getSavedLastPriceData(node) {
const priceData = node.context().get("lastPriceData", node.contextStorage);
const source = node.context().get("lastSource", node.contextStorage);
return { priceData, source };
}
function saveLastPriceData(node, priceData, source) {
node.context().set("lastPriceData", priceData, node.contextStorage);
node.context().set("lastSource", source, node.contextStorage);
}
// Other
function addLastSwitchIfNoSchedule(schedule, hours, config) {
if (!hours.length) {
return;
}
if (schedule.length > 0 && schedule[schedule.length - 1].value === config.outputIfNoSchedule) {
return;
}
const nextHour = DateTime.fromISO(hours[hours.length - 1].start).plus({ hours: 1 });
schedule.push({ time: nextHour.toISO(), value: config.outputIfNoSchedule, countHours: null });
}
function loadDataJustBefore(node, dateDayBefore) {
const dataDayBefore = loadDayData(node, dateDayBefore);
return {
schedule: [...dataDayBefore.schedule],
hours: [...dataDayBefore.hours],
};
}
function deleteSavedScheduleBefore(node, day, checkDays = 0) {
let date = day;
let data = null;
let count = 0;
do {
date = date.plus({ days: -1 });
data = node.context().get(date.toISODate(), node.contextStorage);
node.context().set(date.toISODate(), undefined, node.contextStorage);
count++;
} while (data !== undefined || count <= checkDays);
}
function saveDayData(node, date, plan) {
node.context().set(date, plan, node.contextStorage);
}
function validateInput(node, msg) {
if (!msg.payload) {
validationFailure(node, "No payload");
return;
}
if (typeof msg.payload !== "object") {
validationFailure(node, "Payload is not an object");
return;
}
if (msg.payload.config !== undefined) {
return true; // Got config msg
}
if (msg.payload.commands !== undefined) {
return true; // Got command msg
}
if (msg.payload.priceData === undefined) {
validationFailure(node, "Payload is missing priceData");
return;
}
if (msg.payload.priceData.length === undefined) {
validationFailure(node, "Illegal priceData in payload. Did you use the receive-price node?", "Illegal payload");
return;
}
if (msg.payload.priceData.length === 0) {
validationFailure(node, "priceData is empty");
return;
}
msg.payload.priceData.forEach((h) => {
if (!h.start || isNaN(h.value)) {
validationFailure(node, "Malformed entries in priceData. All entries must contain start and value.");
return;
}
});
return true;
}
module.exports = {
addLastSwitchIfNoSchedule,
getCommands,
handleStrategyInput,
validateInput,
};