Here’s version 2 of the data-logging function node in nodered. It receives data from tasmota through mqtt and processes it in one of two ways.
If somehow the pzem’s total kwh reading resets or is lower than the last recorded reading, then it pushes out an update back to the tasmota device, setting the correct total kwh reading.
Otherwise, it sends off data to an influxdb out node for logging.
So this function node has two outputs.
// msg.topic = ee/battery/tele/RM01LFP16S100A/meter/SENSOR
// 10-second update interval
// Current always positive
// idle current is 1A
if (!msg.payload?.ENERGY || !msg.payload?.ANALOG?.CTEnergy) return [null, null];
const battery_id = msg.topic?.split('/')?.[3];
if (!battery_id) return [null, null];
const [cellCount, ampHours] = battery_id.match(/\d+/g)?.map(Number).slice(1) ?? [];
if (!cellCount || !ampHours) return [null, null];
const measurements = flow.get(battery_id) || {
bat_voltage: 0,
bat_current: 0,
bat_power_w: 0,
cycle_count: 0,
cha_nrg_kwh: 0,
dch_nrg_kwh: 0,
soc_percent: 0,
soc_nrg_kwh: 0,
soh_percent: 0,
soh_nrg_kwh: 0,
tot_nrg_kwh: 0,
dod_nrg_kwh: 0
};
const designCapacity = 3.2 * cellCount * ampHours / 1000;
const actualCapacity = measurements.soh_nrg_kwh || designCapacity;
const { Voltage, Current, Power, Total } = msg.payload.ENERGY;
if (Total < measurements.tot_nrg_kwh) {
msg.payload = `EnergyToday 0; EnergyTotal ${measurements.tot_nrg_kwh * 1000}`
msg.topic = msg.topic.replace('SENSOR', 'Backlog')
msg.topic = msg.topic.replace('tele', 'cmnd')
return [null, msg];
}
measurements.bat_voltage = Voltage;
measurements.bat_current = Current;
measurements.bat_power_w = Power;
const fullCharge = Voltage > 3.45 * cellCount;
const lowBattery = Voltage < 3 * cellCount;
const gridSupply = msg.payload.ANALOG.CTEnergy.Current > 1;
const delta = Current < 1 ? 0 : Total - measurements.tot_nrg_kwh;
measurements.tot_nrg_kwh = Total;
if (gridSupply) {
measurements.soc_nrg_kwh += delta;
measurements.cha_nrg_kwh += delta;
measurements.dod_nrg_kwh = 0;
} else {
measurements.cycle_count += delta / actualCapacity;
measurements.soc_nrg_kwh -= delta;
measurements.dch_nrg_kwh += delta;
measurements.dod_nrg_kwh += delta;
}
measurements.soc_percent = measurements.soc_nrg_kwh / actualCapacity * 100;
if (gridSupply && fullCharge && delta === 0) {
measurements.soc_nrg_kwh = actualCapacity;
measurements.soc_percent = 100;
}
if (!gridSupply && lowBattery) {
measurements.soh_percent = measurements.dod_nrg_kwh / designCapacity * 100;
measurements.soh_nrg_kwh = measurements.dod_nrg_kwh;
measurements.soc_percent = 0;
measurements.soc_nrg_kwh = 0;
}
measurements.soc_nrg_kwh = Math.min(Math.max(measurements.soc_nrg_kwh, 0), actualCapacity);
measurements.soc_percent = Math.min(Math.max(measurements.soc_percent, 0), 100);
flow.set(battery_id, measurements);
const mount_type = battery_id.substring(0, 2);
const chemistry = battery_id.substring(4, 7);
msg.payload = [
measurements,
{
battery_id,
mount_type,
chemistry
}
]
return [msg, null]
Some notes/comments:
The measurements object contains a preliminary set of values, it took a while to come up 12 different variable names that were exactly 11 characters long haha.
I added units in the variable names for power and energy, I imagine I might use them in graphs somewhere. I learned that NRG is a clever way to say energy.
For now, we’re assuming we’re using LFP chemistry so calculations are based on that, this will need to be changed or put into a variable at some point in the future.
I changed the name of mainsAC to gridSupply as that made more sense for a boolean value.
The datasheet for the cells that I’m using specify that charging should be cut when the current drops to 5A, this is called tail current. But since my inverter was designed for lead acid batteries, it eventually switches to float charging and keeps putting out power to the batteries (1w to 2w) which skews the round-trip efficiency values (cha_nrg_kwh and dch_nrg_kwh) over the course of a few days.
I’m calling this insignificant current as “idle current” and put it at 1A for now. Any charge/discharge below 1A won’t be counted.
A non-zero value for dod_nrg_kwh indicates a power outage in effect, I thought that was unexpectedly useful.
The rest of the code is self-explanatory. You could run it through ChatGPT and get a comprehensive explanation if you like.
For the moment the data is dumped every 10 seconds into a bucket in influxdb that has a ttl of 30 days. Maybe I’ll change that to 60 seconds, if the bucket gets too big with multiple batteries. Later, I’m going to have to do an hourly average/max aggregate of this data into another bucket for long term storage.
I’m also going to have to do something about unintended restarts of this nodered instance, it should pull data from influxdb whenever that happens.