8 jul 2025

Automatizando el aprovechamiento solar con baterías con NODE-RED y Victron Multiplus II GX

En la entrada anterior, estuvimos comentando los problemas del sistema dinámico de almacenamiento de energía (DESS) de Victron, por lo que he optado por aprender a usar el sistema de programación visual mediante nodos node-RED, y ayudarme del Gemini 2.5 PRO para crear y optimizar el sistema a mi gusto, teniendo en cuenta que  mi tarifa eléctrica es de tres períodos fijos:

Entre semana:

From To Price

00:00 08:00 €0.097

08:00 10:00 €0.132

10:00 14:00 €0.204

14:00 18:00 €0.132

18:00 22:00 €0.204

22:00 23:59 €0.132


Coste fin de semana:

From To Price

00:00 23:59 €0.097


Imagina que tu sistema de paneles solares y batería tiene un cerebro artificial que trabaja 24/7 para que pagues lo mínimo posible en tu factura de la luz. Eso es exactamente lo que hace este programa.

En lugar de seguir reglas simples, este cerebro toma decisiones inteligentes basándose en tres datos clave:

  1. La Previsión del Tiempo: Sabe con antelación cuánto sol va a hacer y, por tanto, cuánta energía "gratis" vas a generar (datos proporcionados por el propio Multiplus 2 GX).

  2. Tus Hábitos de Consumo: Aprende cuánta electricidad sueles gastar en casa a cada hora del día, cada día de la semana (también obtenidas del mismo sistema).

  3. El Precio de la Luz: Conoce perfectamente cuándo la electricidad es cara (punta), barata (valle) o de precio medio (llano).


¿Cómo Funciona en un Día Normal (Laborable)?

El programa tiene una estrategia principal que se divide en dos fases:

1. El Plan Maestro de la Madrugada (a las 04:00 AM)

Mientras duermes, en la hora más barata del día, el sistema se hace una pregunta clave: "Con el sol que se espera hoy y el consumo que tendrá la casa, ¿nos llegará la energía o nos quedaremos cortos?"

  • Si la respuesta es que el sol no será suficiente, el sistema calcula exactamente cuánta energía faltará para todo el día y aprovecha ese momento para cargar la batería desde la red al precio más bajo posible. Así, "compra por adelantado" la energía que sabe que necesitará más tarde a un precio mucho más caro.

2. Ajustes Inteligentes Durante el Día (cada 30 minutos)

A partir de las 8 de la mañana, el sistema entra en un modo de "optimización continua". Cada media hora, simula el resto del día y se pregunta:

  • "Viendo lo que queda de sol y de consumo, ¿necesitamos hacer algún pequeño ajuste para evitar comprar energía en el próximo pico de precios?"

Si detecta que, por ejemplo, a las 7 de la tarde habrá un déficit, buscará la hora más barata entre el momento actual y ese pico para hacer una pequeña recarga, asegurándose de usar siempre la energía de la forma más económica.


¿Y los Fines de Semana?

Durante el fin de semana, la electricidad es siempre barata. Por lo tanto, la estrategia cambia:

  • El objetivo principal es no desperdiciar ni un rayo de sol. El sistema mantiene la batería con un nivel de carga muy bajo (20%), dejando muchísimo espacio libre (hasta un 80%) para que se llene con la energía solar gratuita y no consumida que se genere.

  • Solo si detecta que va a ser un día muy nublado, mantiene un pequeño colchón de seguridad en la batería (40%) para asegurar el suministro.


En Resumen

Este programa convierte tu instalación solar en un sistema proactivo e inteligente. En lugar de simplemente reaccionar, se anticipa a las necesidades energéticas y a los costes del mercado, asegurando que siempre uses la energía más barata disponible, ya sea la del sol (gratis) o la de la red en su hora más económica. Todo de forma completamente automática.

El programa principal, en la función "decidir qué porcentaje cargar" es el siguiente, a falta de probar extensivamente y mejorar más:

// --- LÓGICA DE PRECIOS Y HORARIOS (Sin cambios) ---

const TARIFAS = { P1: 0.204, P2: 0.132, P3: 0.097 };

const HORARIO_LABORABLE = [

    { hasta: 8, tarifa: TARIFAS.P3 }, { hasta: 10, tarifa: TARIFAS.P2 },

    { hasta: 14, tarifa: TARIFAS.P1 }, { hasta: 18, tarifa: TARIFAS.P2 },

    { hasta: 22, tarifa: TARIFAS.P1 }, { hasta: 24, tarifa: TARIFAS.P2 }

];

function obtenerTarifa(fecha) {

    const dia = fecha.getDay(); const hora = fecha.getHours();

    const esFinDeSemanaEspecial = (dia === 6 || dia === 0 || (dia === 1 && hora < 8));

    if (esFinDeSemanaEspecial) { return TARIFAS.P3; }

    for (const tramo of HORARIO_LABORABLE) { if (hora < tramo.hasta) return tramo.tarifa; }

    return TARIFAS.P2;

}


// --- LÓGICA DE MARGEN SOLAR DINÁMICO (Sin cambios) ---

function obtenerMargenSolar(previsionKwh) {

    if (previsionKwh < 4) return 95;

    if (previsionKwh < 9) return 95 - ((previsionKwh - 4) * 1);

    if (previsionKwh <= 12) return 90;

    if (previsionKwh < 20) return 90 - ((previsionKwh - 12) * (5 / 8));

    return 85;

}


// --- CONFIGURACIÓN PRINCIPAL ---

const CAPACIDAD_BATERIA = 15;

const SOC_MINIMO_BASE = 20;

const UMBRAL_POCO_SOL_KWH = 4;


// --- OBTENCIÓN DE DATOS Y CÁLCULOS INICIALES ---

const socActual = global.get('current_soc') || 0;

const previsionSolarArray = global.get('forecast_solar_array') || [];

const previsionConsumoArray = global.get('forecast_consumo_array') || [];

const ahora = new Date();

const horaActual = ahora.getHours();

const solarTotalDiaKwh = previsionSolarArray.reduce((total, p) => total + p[1], 0) / 1000;


// Objeto para guardar la información de depuración

let debugInfo = {

    timestamp: ahora.toISOString(),

    entradas: { socActual: socActual, solarPrevistoKwh: parseFloat(solarTotalDiaKwh.toFixed(2)) },

    reglaAplicada: "DEFECTO",

    calculos: {}

};


let socObjetivo = SOC_MINIMO_BASE;


// --- LÓGICA DE DECISIÓN ---


const dia = ahora.getDay();

const esFinDeSemana = (dia === 6 || dia === 0 || (dia === 1 && horaActual < 8));

debugInfo.entradas.esFinDeSemana = esFinDeSemana;


if (esFinDeSemana) {

    // Lógica de Fin de Semana (sin cambios)

    debugInfo.reglaAplicada = "FIN_DE_SEMANA_DEFAULT";

    socObjetivo = SOC_MINIMO_BASE; 

    if (solarTotalDiaKwh < UMBRAL_POCO_SOL_KWH && horaActual >= 10 && horaActual < 17) {

        socObjetivo = 40;

        debugInfo.reglaAplicada = "FIN_DE_SEMANA_POCO_SOL";

    }


} else {

    // --- ESTRATEGIA DE DÍA LABORABLE ---

    const tarifaActual = obtenerTarifa(ahora);

    const MARGEN_SOLAR_SOC = obtenerMargenSolar(solarTotalDiaKwh);

    debugInfo.entradas.tarifaActual = tarifaActual;

    debugInfo.entradas.socMaximoPermitido = MARGEN_SOLAR_SOC;

    

    const proximaHora = new Date(ahora.getTime() + 3600 * 1000);

    if (obtenerTarifa(proximaHora) > tarifaActual || tarifaActual === TARIFAS.P1) {

        socObjetivo = SOC_MINIMO_BASE;

        debugInfo.reglaAplicada = "REINICIO_HORA_CARA";

    }

    else if (horaActual === 4) {

        // Lógica de Carga Principal (sin cambios)

        debugInfo.reglaAplicada = "CARGA_PRINCIPAL_4AM";

        const consumoTotalDiaWh = previsionConsumoArray.reduce((total, p) => total + p[1], 0);

        const deficitDiarioKwh = (consumoTotalDiaWh - (solarTotalDiaKwh * 1000)) / 1000;

        debugInfo.entradas.consumoPrevistoKwh = parseFloat((consumoTotalDiaWh/1000).toFixed(2));

        debugInfo.calculos = { deficitDiarioKwh: parseFloat(deficitDiarioKwh.toFixed(2)) };


        if (deficitDiarioKwh > 0) {

            let socCalculado = (deficitDiarioKwh / CAPACIDAD_BATERIA) * 100;

            socObjetivo = Math.min(socCalculado, MARGEN_SOLAR_SOC);

        }

    }

    else if (horaActual > 4 && horaActual < 7) {

        // Lógica de Mantenimiento de Carga (sin cambios)

        debugInfo.reglaAplicada = "MANTENER_CARGA_PRINCIPAL";

    }

    else if (horaActual >= 8) {

        // Lógica de Ajuste Predictivo

        debugInfo.reglaAplicada = "AJUSTE_PREDICTIVO";

        let energiaNecesariaExtraKwh = 0;

        let socSimulado = socActual;


        // Simulación desde la hora actual hasta la medianoche (23:00)

        for (let h = horaActual; h < 24; h++) {

            // Ahora 'h' (la hora del día) se usa directamente como índice del array.

            const consumoHoraWh = (previsionConsumoArray[h] || [0,0])[1];

            const solarHoraWh = (previsionSolarArray[h] || [0,0])[1];

            

            const balanceHoraWh = solarHoraWh - consumoHoraWh;

            const cambioSoc = (balanceHoraWh / 1000 / CAPACIDAD_BATERIA) * 100;

            socSimulado += cambioSoc;

            socSimulado = Math.min(socSimulado, MARGEN_SOLAR_SOC);


            if (socSimulado < SOC_MINIMO_BASE) {

                let deficitCriticoKwh = ((SOC_MINIMO_BASE - socSimulado) / 100) * CAPACIDAD_BATERIA;

                energiaNecesariaExtraKwh = Math.max(energiaNecesariaExtraKwh, deficitCriticoKwh);

                socSimulado = SOC_MINIMO_BASE; 

            }

        }

        

        debugInfo.calculos = { energiaExtraRequeridaKwh: parseFloat(energiaNecesariaExtraKwh.toFixed(2)) };


        if (energiaNecesariaExtraKwh > 0) {

            const energiaActualKwh = (socActual / 100) * CAPACIDAD_BATERIA;

            const energiaObjetivoKwh = energiaActualKwh + energiaNecesariaExtraKwh;

            let socCalculado = (energiaObjetivoKwh / CAPACIDAD_BATERIA) * 100;

            socObjetivo = Math.min(socCalculado, MARGEN_SOLAR_SOC);

        }

    }

}


// --- PREPARACIÓN DEL MENSAJE DE SALIDA ---

const socFinal = parseFloat(socObjetivo.toFixed(0));

debugInfo.socObjetivoFinal = socFinal;

const ultimoSocEnviado = flow.get('ultimoSocEnviado') || SOC_MINIMO_BASE;

debugInfo.ultimoSocEnviado = ultimoSocEnviado;


let msgSalida1 = null; 

if (socFinal !== ultimoSocEnviado && debugInfo.reglaAplicada !== "MANTENER_CARGA_PRINCIPAL") {

    msgSalida1 = { payload: socFinal };

    flow.set('ultimoSocEnviado', socFinal);

    debugInfo.accion = `Enviando nuevo SOC: ${socFinal}`;

} else {

    debugInfo.accion = `Sin cambios, se mantiene el SOC anterior: ${ultimoSocEnviado}`;

}


let msgSalida2 = { payload: `Regla: ${debugInfo.reglaAplicada}`, debug_info: debugInfo };


return [msgSalida1, msgSalida2];

Explicación:

El código aprovecha muy bien las horas más baratas para la carga principal, aunque los ajustes posteriores podrían ser aún más "oportunistas". Te lo detallo:

Carga Principal (Muy Bien Optimizada)

La regla CARGA_PRINCIPAL_4AM es la más importante y está perfectamente diseñada para aprovechar el coste más bajo.

  • Se ejecuta a las 4 de la mañana, en mitad del período "Valle" (P3), que es el más económico (€0.097/kWh).

  • En ese momento, evalúa el déficit energético de todo el día (Consumo total - Sol total).

  • Si necesita cargar, lo hace en ese instante, asegurando que la mayor parte de la energía que compres a la red se adquiera al precio más bajo posible.

Esta es la estrategia más eficiente, ya que realiza la "compra" de energía más grande cuando es más barata.


Ajustes Durante el Día (Bien, pero con Potencial de Optimización Avanzada)

La regla AJUSTE_PREDICTIVO (que se ejecuta cada media hora a partir de las 08:00) también busca la economía, pero de una forma diferente.

  • Su función es de "seguridad": simula el resto del día y, si detecta que la energía almacenada y la solar prevista no serán suficientes para cubrir un pico de consumo futuro, decide cargar.

  • Actúa de forma preventiva: La decisión de cargar la toma en el momento de la detección. Por ejemplo, si a las 9:00 (período "Llano" - P2) detecta que te quedarás corto a las 20:00 (período "Punta" - P1), iniciará una carga de ajuste a las 9:00.

¿Se aprovechan las horas más baratas? Sí, en el sentido de que evita a toda costa comprar energía en las horas "Punta". La carga de ajuste siempre se realizará en un período "Llano" o "Valle" para cubrir una necesidad en un período "Punta".

El algoritmo actual no llega a comparar si, por ejemplo, sería mejor esperar a otro período "Llano" más adelante para hacer ese ajuste. Sin embargo, para la gran mayoría de los casos, la estrategia actual es extremadamente efectiva, ya que la carga principal y más grande ya se ha realizado al coste mínimo posible.

¿Cómo se Evita Cargar en las Horas de Más Precio?

El sistema lo evita gracias a una regla prioritaria que se ejecuta antes que el ajuste predictivo.

Observa el orden de la lógica para los días laborables:

  1. Primero, se ejecuta la REGLA PRIORITARIA: REINICIO_HORA_CARA. Esta línea es la clave:

    JavaScript
    if (obtenerTarifa(proximaHora) > tarifaActual || tarifaActual === TARIFAS.P1) {
        socObjetivo = SOC_MINIMO_BASE;
        debugInfo.reglaAplicada = "REINICIO_HORA_CARA";
    }
    

    Esta regla comprueba si la hora actual ya está en el período más caro (tarifaActual === TARIFAS.P1). Si es así, fija el SOC al 20% y la lógica de ese ciclo termina.

  2. Después, si la primera regla no se cumple, se pasa a las siguientes (else if...). El AJUSTE_PREDICTIVO solo se ejecuta si la hora actual no es la más cara.

En resumen, la regla de ajuste predictivo nunca tiene la oportunidad de ejecutarse durante las horas punta, porque una regla con mayor prioridad ya ha tomado la decisión de no cargar.


¿Cómo se Tiene en Cuenta la Energía de Todo el Período Caro?

El algoritmo ya tiene en cuenta la energía total necesaria para superar todo el período caro (y, de hecho, todo lo que resta de día). Lo hace a través de la simulación.

Imagina que son las 16:00 y el período caro es de 18:00 a 22:00. Así funciona la simulación:

  1. Batería Virtual: El código crea una "batería virtual" (socSimulado) que empieza con el SOC real.

  2. Viaje al Futuro: El bucle for recorre el futuro hora por hora (16:00, 17:00, 18:00, 19:00...).

  3. Cálculo de Déficit: En cada hora futura, resta el consumo y suma el sol para ver cómo queda la batería virtual.

  4. Detección del Punto Más Bajo: Si a las 19:00 la batería virtual baja a 15%, el sistema anota que necesita un 5% extra. Si luego, a las 21:00, la simulación muestra que la batería bajaría hasta un 10%, el sistema actualiza su necesidad y anota que el déficit máximo es del 10%.

  5. Decisión Final: Al final de la simulación, el sistema sabe que el punto más crítico requiere un 10% adicional de energía para no bajar de 20%. Esa es la cantidad total que decidirá cargar de forma anticipada.

Por lo tanto, al simular todo el período y quedarse siempre con el déficit máximo acumulado (Math.max), el algoritmo se asegura de tener energía suficiente para superar todas las horas del período caro, no solo la primera.

Nota: Este sistema sólo funciona correctamente tras un tiempo de uso del VICTRON, cuando sus previsiones a través del portal VRM son bastante fiables, y si tienes configurado correctamente la localización del sistema, para evaluar la previsión del tiempo de forma confiable.

Aquí podéis descargaros el archivo json para importarlo y adaptarlo a vuestras necesidades:

Programa json node-red

¡Espero vuestros comentarios!

Hilo relacionado: DESS necesita una reprogramación desde 0

Ideas recopiladas de:

https://community.victronenergy.com/t/weather-prediction-in-node-red/19965/22


No hay comentarios:

Publicar un comentario

Puede dejar su comentario, que tratará de ser moderado en los días siguientes. En caso de ser algo importante/urgente, por favor utilicen el formulario de arriba a la derecha para contactar.