Node-RED flow analysis: This high-tech horse ranch uses Node-Red to automate its solar power

In the comments under https://www.youtube.com/watch?v=H3JuXYja4Ds there is a request for the used flows. As I can only share parts of the flows and the YouTube comments don’t allow for pictures / screenshots to pinpoint what parts of the flow, I thought I might as well start a community post on this.

First of all the solar forecasting. This is done using the victron-vrm-api node, which is already part of the Node-RED install on Venus.

These are all fed by an inject node that inject once after 10 seconds and then at an interval of every 5 minutes.

The vrm-api-node are all set to fetch different attributes, but are also configured to get the VRM site id from the flow context variable {{flow.Instanzid}}.

Then the topics are all modified for each of the stats, before being fed into a join node, that combines the output of all 4 streams into one again.

From there it goes into a few nodes before entering the chart node, from node-red-dashboard

The first function calculates the amount of excess solar:

// Das Skript analysiert und berechnet die voraussichtliche Energieerzeugung aus Solaranlagen und den geschätzten Energieverbrauch

const d = new Date();
let day = d.getDay();

//Werte einlesen des Forcasts

let est_consumption_hours = [
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0
];

let est_solarertrag_hours = [
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0
];

let consumption_hours = [
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0
];

let solarertrag_hours = [
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0,
    0
];

function parseRecords(hours, records) {
    for(let i = 0; i < records.length; i++)
    {
        const d = new Date(records[i][0]);
        const hour = d.getHours();
        hours[hour] += records[i][1] / 1000;
    }
}

parseRecords(est_consumption_hours, msg.payload.consumption_fc.records.vrm_consumption_fc);
parseRecords(est_solarertrag_hours, msg.payload.solarertrag_fc.records.solar_yield_forecast);
parseRecords(consumption_hours, msg.payload.consumption.records.consumption);
parseRecords(solarertrag_hours, msg.payload.solarertrag.records.solar_yield);

// Totale berechnen
const est_consumption_total = Math.round((msg.payload.consumption_fc.totals.vrm_consumption_fc) / 1000 * 100) / 100; // kWh
const est_solarertrag_total = Math.round((msg.payload.solarertrag_fc.totals.solar_yield_forecast) / 1000 * 100) / 100; // kWh
const consumption_total = Math.round((msg.payload.consumption.totals.consumption) / 1000 * 100) / 100; // kWh
const solarertrag_total = Math.round((msg.payload.solarertrag.totals.solar_yield) / 1000 * 100) / 100; // kWh



// Für jede Stunde wird überprüft, ob die Solarerzeugung höher als der geschätzte Verbrauch ist. Wenn ja, wird der Überschuss berechnet und summiert.
// Der gesamte Solarüberschuss (solar_excess) wird ebenfalls auf zwei Dezimalstellen gerundet.

let est_solar_excess = 0;
for(let i = 0; i < est_solarertrag_hours.length; i++)
{
    if(est_solarertrag_hours[i] > est_consumption_hours[i])
    {
        est_solar_excess += est_solarertrag_hours[i] - est_consumption_hours[i];
    }
}
est_solar_excess = Math.round(est_solar_excess * 100) / 100


let solar_excess = 0;
for(let i = 0; i < solarertrag_hours.length; i++)
{
    if(solarertrag_hours[i] > consumption_hours[i])
    {
        solar_excess += solarertrag_hours[i] - consumption_hours[i];
    }
}
solar_excess = Math.round(solar_excess * 100) / 100


// Der Solarüberschuss wird als msg.payload festgelegt.
// Die Gesamtwerte für die Solarerzeugung und den Solarüberschuss werden im Datenfluss (flow) gespeichert.

msg.payload = est_solar_excess;

let solar_excess_hours = new Array(24).fill(0);
let est_solar_excess_hours = new Array(24).fill(0);

for(let i = 0; i < 24; i++) {
    if(solarertrag_hours[i] > consumption_hours[i]) {
        solar_excess_hours[i] = solarertrag_hours[i] - consumption_hours[i];
    }
    if(est_solarertrag_hours[i] > est_consumption_hours[i]) {
        est_solar_excess_hours[i] = est_solarertrag_hours[i] - est_consumption_hours[i];
    }
}



flow.set('est_solarertrag_total', est_solarertrag_total);
flow.set('est_consumption_total', est_consumption_total)
flow.set('solarertrag_total', solarertrag_total);
flow.set('consumption_total', consumption_total)
flow.set('est_solar_excess', est_solar_excess);
global.set('est_solar_excess', est_solar_excess);
flow.set('solar_excess', solar_excess);


//Daten für Grafik
// Zeitzone Offset in Stunden (z.B. +1 für CET)
const timezoneOffset = 1;

// Vorbereitung der Daten für die Grafik-Node
let chartData_all = {
    series: ["FC Verbrauch", "FC Solarertrag", "Verbrauch", "Solarertrag"],
    data: [[], [], [], []],
    labels: ["Stunde"]
};

let chartData_fc_solar = {
    series: ["FC Solarertrag", "Solarertrag"],
    data: [[], []],
    labels: ["Stunde"]
};

let chartData_fc_consumption = {
    series: ["FC Verbrauch", "Verbrauch"],
    data: [[], []],
    labels: ["Stunde"]
};

let chartData_fc_excess = {
    series: ["FC Überschuss", "Überschuss"],
    data: [[], []],
    labels: ["Stunde"]
};

// Erstellen des Datumsformats für jede Stunde mit Zeitzone
for (let i = 0; i < 24; i++) {
    let date = new Date();
    date.setHours(i, 0, 0, 0);

    // Anpassen der Zeitzone
    date.setHours(date.getHours() + timezoneOffset);

    // Konvertieren des Datums in das gewünschte Format
    let dateString = date.toISOString().replace('Z', '+01:00');

    // Daten für alle Arrays
    chartData_all.data[0].push({ x: dateString, y: est_consumption_hours[i] });
    chartData_all.data[1].push({ x: dateString, y: est_solarertrag_hours[i] });
    chartData_all.data[2].push({ x: dateString, y: consumption_hours[i] });
    chartData_all.data[3].push({ x: dateString, y: solarertrag_hours[i] });

    // Daten nur für Solar (FC und tatsächlich)
    chartData_fc_solar.data[0].push({ x: dateString, y: est_solarertrag_hours[i] });
    chartData_fc_solar.data[1].push({ x: dateString, y: solarertrag_hours[i] });

    // Daten nur für Verbrauch (FC und tatsächlich)
    chartData_fc_consumption.data[0].push({ x: dateString, y: est_consumption_hours[i] });
    chartData_fc_consumption.data[1].push({ x: dateString, y: consumption_hours[i] });
    
    chartData_fc_excess.data[0].push({ x: dateString, y: est_solar_excess_hours[i] });
    chartData_fc_excess.data[1].push({ x: dateString, y: solar_excess_hours[i] });
}

// Setzen der vorbereiteten Daten als msg.chartData
msg.chartData_all = chartData_all;
msg.chartData_fc_solar = chartData_fc_solar;
msg.chartData_fc_consumption = chartData_fc_consumption;
msg.chartData_fc_excess = chartData_fc_excess;


// Ausgabe
node.status({text:"FC-Verbr: " + est_consumption_total + " | FC-Sol: " + est_solarertrag_total + " | FC Sol-Übersch: " + solar_excess});

return msg;

And the second one converts the data for the chart to parse.

// Umwandeln von msg.chartData in das gewünschte Array-Format
msg.payload = [{
    "series": msg.chartData_fc_solar.series,
    "data": msg.chartData_fc_solar.data,
    "labels": msg.chartData_fc_solar.labels
}];

// Entfernen aller anderen Eigenschaften aus dem msg-Objekt
msg = { payload: msg.payload };

return msg;

Then there is another limiter and lastly the charts, which doesn’t seem to have too many noticeable settings, other than perhaps the interpolation set to bezier..

Comment below if there are more questions on the flows in the clip.

2 Likes