Created
December 6, 2024 09:40
-
-
Save M4GNV5/37c44632513af4ac775f146bf7d2a226 to your computer and use it in GitHub Desktop.
node-red node I am using for optimizing PV energy usage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// output nodes: [miner on/off, ventilation target, temperature target, user message] | |
const heatpumpEnabled = global.get("enablePvHeating"); | |
const chargingEnabled = global.get("enablePvCharging"); | |
const minerEnabled = global.get("enablePvMining"); | |
const minTemperature = parseFloat(global.get("minTemperature")); | |
const dayMinTemperature = parseFloat(global.get("dayMinTemperature")); | |
let targetTemperature = parseFloat(global.get("targetTemperature")); | |
const forceVent = parseFloat(global.get("forceVent")); | |
const minerRunning = flow.get("minerRunning"); | |
const evCharging = flow.get("evCharging"); | |
const gridPower = parseFloat(flow.get("gridPower")); | |
const indoorTemp = parseFloat(flow.get("indoorTemp")); | |
const ventTarget = parseFloat(flow.get("nilanVentTarget")); | |
const nilanTempTarget = parseFloat(flow.get("nilanTempTarget")); | |
const nilanMode = flow.get("nilanMode"); | |
const hour = (new Date()).getHours(); | |
const currentMinTemp = (hour >= 8 && hour <= 16) ? dayMinTemperature : minTemperature; | |
const hasToHeat = indoorTemp < currentMinTemp | |
|| (ventTarget > 0 && nilanTempTarget > 15 && indoorTemp < currentMinTemp + 2); | |
const hasToVent = forceVent !== -1 && (ventTarget === 15 || ventTarget === 28); | |
if (hasToHeat && targetTemperature > currentMinTemp + 2) | |
targetTemperature = currentMinTemp + 2; | |
const controlOutputs = new Array(node.outputCount); | |
const controlableDevices = [ | |
{ | |
name: "Twizy", | |
// TODO: hasToConsume | |
canConsumeMore: chargingEnabled && !evCharging, | |
canConsumeLess: chargingEnabled && evCharging, | |
consumption: 450, // TODO dynamically adjust consumption | |
consumeMore: () => controlOutputs[4] = { payload: true }, | |
consumeLess: () => controlOutputs[4] = { payload: false }, | |
}, | |
{ | |
name: "Heatpump", | |
hasToConsume: hasToHeat || hasToVent, | |
canConsumeMore: heatpumpEnabled && ( | |
ventTarget == 0 // we are currently off | |
|| nilanTempTarget !== targetTemperature // we are currently venting only | |
|| (indoorTemp > targetTemperature) != (nilanMode === 'cool') // heat/cool is wrong | |
), | |
canConsumeLess: heatpumpEnabled && ventTarget > 0, | |
consumption: (function() { | |
if(ventTarget == 0) | |
return 80 // 80W from off to vent only | |
else if (nilanTempTarget != targetTemperature && indoorTemp > targetTemperature) | |
return 500 // 500W from vent only to cool // TODO: validate this value | |
else if (nilanTempTarget != targetTemperature && indoorTemp < targetTemperature) | |
return 400 // 400W from vent only to heat // TODO: validate this value | |
else | |
return 0 // make it always possible to switch between heat/cool | |
})(), | |
consumeMore: () => { | |
controlOutputs[2] = { payload: 1 } // vent 1 | |
controlOutputs[1] = { payload: indoorTemp > targetTemperature ? 'cool' : 'heat' } | |
if (ventTarget == 0 && indoorTemp < targetTemperature) { | |
controlOutputs[3] = { payload: 15 } // temperature: 15 | |
} | |
else if (ventTarget == 0 && indoorTemp > targetTemperature) { | |
controlOutputs[3] = { payload: 28 } // temperature: 28 | |
} | |
else { | |
controlOutputs[3] = { payload: targetTemperature } | |
} | |
}, | |
consumeLess: () => { | |
if (nilanMode === 'heat' && nilanTempTarget !== 15) { | |
// go back to venting only | |
controlOutputs[1] = { payload: 'heat' } // mode: heating | |
controlOutputs[2] = { payload: 1 } // vent 1 | |
controlOutputs[3] = { payload: 15 } // temperature: 15 | |
} | |
else if (nilanMode === 'cool' && nilanTempTarget !== 28) { | |
controlOutputs[1] = { payload: 'cool' } // mode: cooling | |
controlOutputs[2] = { payload: 1 } // vent 1 | |
controlOutputs[3] = { payload: 28 } // temperature: 28 | |
} | |
else { | |
// turn off | |
controlOutputs[1] = { payload: 'auto' } // mode: heating | |
controlOutputs[2] = { payload: 0 } // vent 0 | |
controlOutputs[3] = { payload: targetTemperature } // temperature: 15 | |
} | |
}, | |
}, | |
{ | |
name: "Miner", | |
canConsumeMore: minerEnabled && !minerRunning, | |
canConsumeLess: minerEnabled && minerRunning, | |
consumption: 170, | |
consumeMore: () => controlOutputs[0] = { payload: true }, | |
consumeLess: () => controlOutputs[0] = { payload: false }, | |
}, | |
] | |
let lastGridPowers = JSON.parse(context.get("lastGridPowers") || "[]"); | |
lastGridPowers.push({ timestamp: Date.now(), power: gridPower }); | |
lastGridPowers = lastGridPowers.filter(x => Date.now() - x.timestamp < 10 * 60 * 1000); | |
context.set("lastGridPowers", JSON.stringify(lastGridPowers)); | |
const lastChange = context.get("lastChange"); | |
if(Date.now() - lastChange < 10 * 60 * 1000) | |
return new Array(node.outputCount); // do nothing | |
function isPowerAvailable(x) { | |
return lastGridPowers.length > 60 && lastGridPowers.every(y => x < -1 * y.power); | |
} | |
function isConsumingFromGrid() { | |
return lastGridPowers.every(x => x.power > 0); | |
} | |
let change = null | |
for(const device of controlableDevices) { | |
if (device.hasToConsume && device.canConsumeMore) { | |
device.consumeMore(); | |
change = `Forcing consumption of ${device.name} (grid power ${gridPower}, indoor temp ${indoorTemp})`; | |
break; | |
} | |
} | |
if (change === null && isConsumingFromGrid()) { | |
for(let i = controlableDevices.length - 1; i >= 0; i--) { | |
if (!controlableDevices[i].hasToConsume && controlableDevices[i].canConsumeLess) { | |
controlableDevices[i].consumeLess(); | |
change = `Decreasing consumption of ${controlableDevices[i].name} (grid power ${gridPower}, indoor temp ${indoorTemp})`; | |
break; | |
} | |
} | |
} | |
else if (change === null) { | |
for(const device of controlableDevices) { | |
if (device.canConsumeMore && isPowerAvailable(device.consumption)) { | |
device.consumeMore(); | |
change = `Increasing consumption of ${device.name} (grid power ${gridPower}, expecting ${device.consumption}W, indoor temp ${indoorTemp})`; | |
break; | |
} | |
} | |
} | |
if (change) { | |
context.set("lastChange", Date.now()); | |
controlOutputs[5] = { | |
payload: { | |
chatId: 211452800, | |
type: "message", | |
content: change, | |
} | |
}; | |
return controlOutputs; | |
} | |
else { | |
return new Array(node.outputCount); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment