Skip to content

Instantly share code, notes, and snippets.

@M4GNV5
Created December 6, 2024 09:40
Show Gist options
  • Save M4GNV5/37c44632513af4ac775f146bf7d2a226 to your computer and use it in GitHub Desktop.
Save M4GNV5/37c44632513af4ac775f146bf7d2a226 to your computer and use it in GitHub Desktop.
node-red node I am using for optimizing PV energy usage
// 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