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

123 lines (110 loc) 4.06 kB
const { DateTime } = require("luxon"); const { booleanConfig, calcNullSavings, fixOutputValues, saveOriginalConfig } = require("./utils"); const { getBestContinuous, getBestX } = require("./strategy-lowest-price-functions"); const { strategyOnInput } = require("./strategy-functions"); module.exports = function (RED) { function StrategyLowestPriceNode(config) { RED.nodes.createNode(this, config); const node = this; node.status({}); const validConfig = { fromTime: config.fromTime, toTime: config.toTime, hoursOn: parseInt(config.hoursOn), maxPrice: config.maxPrice == null || config.maxPrice == "" ? null : parseFloat(config.maxPrice), doNotSplit: booleanConfig(config.doNotSplit), sendCurrentValueWhenRescheduling: booleanConfig(config.sendCurrentValueWhenRescheduling), outputIfNoSchedule: booleanConfig(config.outputIfNoSchedule), outputOutsidePeriod: booleanConfig(config.outputOutsidePeriod), outputValueForOn: config.outputValueForOn || true, outputValueForOff: config.outputValueForOff || false, outputValueForOntype: config.outputValueForOntype || "bool", outputValueForOfftype: config.outputValueForOfftype || "bool", override: "auto", contextStorage: config.contextStorage || "default", }; fixOutputValues(validConfig); saveOriginalConfig(node, validConfig); node.on("close", function () { clearTimeout(node.schedulingTimeout); }); node.on("input", function (msg) { strategyOnInput(node, msg, doPlanning, calcNullSavings); }); } RED.nodes.registerType("ps-strategy-lowest-price", StrategyLowestPriceNode); }; function doPlanning(node, priceData) { const values = priceData.map((pd) => pd.value); const startTimes = priceData.map((pd) => pd.start); const from = parseInt(node.fromTime); const to = parseInt(node.toTime); const periodStatus = []; const startIndexes = []; const endIndexes = []; let currentStatus = from < (to === 0 && to !== from ? 24 : to) ? "Outside" : "StartMissing"; let hour; startTimes.forEach((st, i) => { hour = DateTime.fromISO(st).hour; if (hour === to && to === from && currentStatus === "Inside") { endIndexes.push(i - 1); } if (hour === to && to !== from && i > 0 ) { if(currentStatus !== "StartMissing") { endIndexes.push(i - 1); } currentStatus = "Outside"; } if (hour === from) { currentStatus = "Inside"; startIndexes.push(i); } periodStatus[i] = currentStatus; }); if (currentStatus === "Inside" && hour !== (to === 0 ? 23 : to - 1)) { // Last period incomplete let i = periodStatus.length - 1; do { periodStatus[i] = "EndMissing"; hour = DateTime.fromISO(startTimes[i]).hour; i--; } while (periodStatus[i] === "Inside" && hour !== from); startIndexes.splice(startIndexes.length - 1, 1); } if (hour === (to === 0 ? 23 : to - 1)) { endIndexes.push(startTimes.length - 1); } const onOff = []; // Set onOff for hours that will not be planned periodStatus.forEach((s, i) => { onOff[i] = s === "Outside" ? node.outputOutsidePeriod : s === "StartMissing" || s === "EndMissing" ? node.outputIfNoSchedule : null; }); startIndexes.forEach((s, i) => { makePlan(node, values, onOff, s, endIndexes[i]); }); return onOff; } function makePlan(node, values, onOff, fromIndex, toIndex) { const valuesInPeriod = values.slice(fromIndex, toIndex + 1); const res = node.doNotSplit ? getBestContinuous(valuesInPeriod, node.hoursOn) : getBestX(valuesInPeriod, node.hoursOn); const sumPriceOn = res.reduce((p, v, i) => { return p + (v ? valuesInPeriod[i] : 0); }, 0); const average = sumPriceOn / node.hoursOn; res.forEach((v, i) => { onOff[fromIndex + i] = node.maxPrice == null ? v : node.doNotSplit ? v && average <= node.maxPrice : v && valuesInPeriod[i] <= node.maxPrice; }); return onOff; }