@gravypower/node-red-franklinwh
Version:
Node-RED node to control FranklinWH gateway
152 lines (128 loc) • 6.64 kB
JavaScript
const apiHandler = require('../../../api-handler');
module.exports = function(RED) {
// Constants for formatting
const POWER_PRECISION = 2; // Decimal places for power values
const ENERGY_PRECISION = 3; // Decimal places for energy values
function formatPower(value) {
if (typeof value !== 'number') return '0';
return value.toFixed(POWER_PRECISION);
}
function formatEnergy(value) {
if (typeof value !== 'number') return '0';
return value.toFixed(ENERGY_PRECISION);
}
function formatDuration(minutes) {
if (minutes < 60) {
return `${Math.round(minutes)}m`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = Math.round(minutes % 60);
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
function calculateTimeEstimate(currentPercentage, targetPercentage, powerOutput, reservePercentage = 5) {
if (!powerOutput) return null;
// Assume 13.6 kWh capacity (standard FranklinWH aPower capacity)
const BATTERY_CAPACITY = 13.6;
const currentKWh = (currentPercentage / 100) * BATTERY_CAPACITY;
const targetKWh = (targetPercentage / 100) * BATTERY_CAPACITY;
const kWhDifference = Math.abs(targetKWh - currentKWh);
// Convert power from kW to rate per hour
const hourlyRate = Math.abs(powerOutput);
if (hourlyRate === 0) return null;
// Calculate hours and convert to minutes
const minutes = (kWhDifference / hourlyRate) * 60;
return formatDuration(minutes);
}
function getStatusColor(percentage) {
if (percentage >= 75) return 'green';
if (percentage >= 25) return 'yellow';
return 'red';
}
function FranklinWHGetBatteryNode(config) {
RED.nodes.createNode(this, config);
const node = this;
const server = RED.nodes.getNode(config.server);
if (!server) {
node.status({fill:"red", shape:"ring", text:"missing config"});
node.error("No server configuration");
return;
}
node.status({fill:"grey", shape:"ring", text:"waiting for input"});
node.on("input", async function (msg, send, done) {
try {
// Show requesting status
node.status({fill:"blue", shape:"dot", text:"requesting..."});
await apiHandler.executeWithRetry(
node,
server,
async (api) => {
// Get battery status using getAGateStatus method
const batteryData = await api.getAGateStatus();
// Map the data to our battery status structure
const batteryStatus = {
chargePercentage: batteryData.chargePercentage,
powerOutput: batteryData.batteryOut,
totalCharged: batteryData.batteryInKWh,
totalDischarged: batteryData.batteryOutKWh
};
// Format and validate the data
const chargePercentage = Math.min(100, Math.max(0,
parseInt(batteryStatus.chargePercentage) || 0));
const powerOutput = parseFloat(batteryStatus.powerOutput) || 0;
const energyCharged = parseFloat(batteryStatus.totalCharged) || 0;
const energyDischarged = parseFloat(batteryStatus.totalDischarged) || 0;
// Show battery status
const statusColor = getStatusColor(chargePercentage);
node.status({
fill: statusColor,
shape: "dot",
text: `${chargePercentage}% (${formatPower(powerOutput)}kW)`
});
// Get reserve level and calculate time estimate
const reserve = await api.getReserve();
const reservePercentage = reserve || 5; // Default to 5% if not set
// Calculate time estimate based on charging direction
let timeEstimate = null;
if (powerOutput !== 0) {
const targetPercentage = powerOutput < 0 ? 100 : reservePercentage;
timeEstimate = calculateTimeEstimate(chargePercentage, targetPercentage, powerOutput, reservePercentage);
}
// Return structured data
return {
state: {
chargePercentage: chargePercentage,
powerOutput: parseFloat(formatPower(powerOutput)),
charging: powerOutput < 0 // Negative power means charging (power flowing into battery)
},
energy: {
charged: parseFloat(formatEnergy(energyCharged)),
discharged: parseFloat(formatEnergy(energyDischarged)),
unit: 'kWh'
},
estimate: {
timeRemaining: timeEstimate,
type: powerOutput < 0 ? 'timeToFull' :
powerOutput > 0 ? 'timeToReserve' : null,
reserveLevel: reservePercentage
}
};
},
msg,
send,
done
);
} catch (err) {
const errorMsg = err.message || 'Unknown error';
const displayMsg = errorMsg.substring(0, 25);
node.status({fill:"red", shape:"ring", text: displayMsg});
node.error("FranklinWH Error: " + errorMsg, msg);
if (done) done(err);
}
});
node.on('close', function(done) {
node.status({});
done();
});
}
RED.nodes.registerType("franklinwh-get-battery", FranklinWHGetBatteryNode);
}