Debido a la canibalización de precios en energía solar, controlar un inversor según el precio de REE y un protocolo como Modbus cobra sentido. Utilizando la plataforma Arduino se consulta el precio en la API de REE y se envían órdenes al inversor.
Introducción
Quien posee una instalación fotovoltaica doméstica con vertido a red, puede ver (según su contrato) que durante determinadas horas paga por inyectar energía a la red. Algo que ocurre especialmente en horas centrales del día debido a precios negativos de la energía.
Las instalaciones de generación fotovoltaica han experimentado un crecimiento casi exponencial desde 2019. Este aumento ha provocado un efecto conocido como «canibalización», donde la cantidad de energía solar que podría generarse es tal que no hay demanda para consumirla. El exceso de oferta hunde los precios, haciendo que el productor solar vea sus ingresos desaparecer precisamente apostando por dicha tecnología. Actualmente, esto ocurre en primavera y otoño, cuando la generación solar ya es importante, pero sin consumos como calefacción o aire acondicionado.

Aprovechando que las instalaciones con potencia inferior a 100kW no tienen compromisos de generación con la Red Eléctrica*, lo más inteligente es detener el vertido cuando el precio no sea interesante.
De esta manera no solo se evita perder dinero, también alargar la vida de la electrónica de potencia. Como se ve al calcular las pérdidas de un inversor monofásico, estas crecen con el cuadrado de la corriente, por ello tiene sentido reducirlas disminuyendo la producción mediante un protocolo como Modbus en función del precio de REE.
Se muestra de forma gráfica la idea que se pretende conseguir.

* Cualquier instalación eléctrica tiene derechos, pero también obligaciones, este artículo tiene un interés puramente académico y depende de cada individuo estar al tanto de sus obligaciones.
Formas de comandar un inversor
Existen diferentes formas de comandar un inversor, en general se encuentran las siguientes:
- Entrada digital: Activando una entrada digital puede ejecutarse alguna acción o pasar a un determinado modo de funcionamiento.
- Conexión TCP: Al conectar el inversor a una red es posible acceder a él mediante distintos protocolos: una API propia del fabricante, Modbus TCP o MQTT.
- Servidor web: Es un caso particular del anterior, pero merece apartado propio. Muchos equipos corren un servidor web donde se aloja una página web. Al entrar desde un navegador es posible realizar cambios y lecturas, cuya facilidad dependerá del cariño que haya puesto la marca en diseñar la página.
- Puerto serie: Es uno de los métodos más utilizados históricamente. Sobre una conexión RS-232, RS-422, RS-485, CAN-BUS, etc. es posible la lectura y escritura de parámetros o estados.

Sin embargo, que exista una vía de acceso real depende de cada fabricante. Una capa hardware estandarizada no asegura que el protocolo de comunicación sea público. Cada método requiere estudiar la documentación y en ocasiones ponerse en contacto con el fabricante bajo NDA. A veces es sencillamente imposible para un usuario utilizar un determinado método.
En este inversor en concreto es posible configurar una entrada para que fije la energía máxima que puede inyectar a red. Bajo TCP ofrece un servidor web, MQTT y Modbus.
Este último método: Modbus TCP, será el utilizado en este artículo. El motivo no es otro que su simplicidad, un microcontrolador sencillo puede implementarlo. Tampoco es necesaria una conexión física con el equipo. El inconveniente es su bajo nivel de seguridad, y es que cualquier persona conectada a la misma red que el inversor podría manipularlo.
Inversor Fronius Primo GEN24 y Modbus
El inversor objetivo es un Fronius Primo GEN24 8.0 PLUS. Si bien cada inversor es un mundo, muchos comparten características y funciones, especialmente las relacionadas con funcionalidad. Por ello, se anima a que cada usuario investigue las posibilidades de su instalación y los componentes que la forman.

Como es de esperar este modelo permite muchas configuraciones e integraciones, entre ellas la generación solar con conexión y vertido a red.

La última parte de este apartado será obviamente el mapa de registros. Este es ofrecido por Fronius bajo petición en este enlace. Cada individuo debe ser responsable de la obtención y protección de dicha documentación bajo los términos que impone Fronius. Sobra decir hoy día, en internet, es posible encontrar casi cualquier tipo de información.
ModBus TCP en Arduino
Modbus es sumamente sencillo y transparente, el protocolo conecta distintos equipos electrónicos de automatización industrial, es fácilmente manejable tanto por humanos como por microcontroladores sencillos. En su creación durante el final de la década de los 70, a la automatización distribuida todavía le quedaba un largo camino hasta la Industria 4.0. El canal físico sobre el que funciona también suele ser simple, siendo los clásicos canales asíncronos RS-232, RS-422 y RS-485 los más utilizados. Toda esta simpleza tiene un precio, su seguridad se basa en la inaccesibilidad de las instalaciones y el desconocimiento del protocolo. Solamente las implementaciones sobre TCP gozan de cierta seguridad, la cual no es más que la propia que puede ofrecer este tipo de redes.
Aunque existen librerías Modbus TCP para Arduino se implementará una propia. Será muy simple, conteniendo lo estrictamente necesario, que es este caso será escribir y leer HoldingRegisters.
Escribir en Holding Registers
El código a continuación realiza una escritura a partir del Holding Register startAddress el número de registros indicado por reg2write. La información a escribir llega desde el buffer tx_buffer. Dicho de otra forma, tx_buffer es transmitido a los Holding Registers.
A diferencia del código del repositorio aquí se omiten las comprobaciones de datos, centrando la atención en la implementación del protocolo.
bool writeHoldingRegisters(WiFiClient& client, uint16_t startAddress, const uint16_t* tx_buffer, uint16_t regs2write){
/*** Check connection and verify data ***/
uint8_t byteCount = regs2write * 2;
uint16_t packetSize = 13 + byteCount; // 13 = header + byteCount byte
byte request[260]; //Safe buffer
//Modbus TCP header
request[0] = 0x00; request[1] = 0x02; // Transaction ID
request[2] = 0x00; request[3] = 0x00; // Protocol ID
request[4] = 0x00; request[5] = 7 + byteCount; // Length = 7 + byte count
request[6] = 0x01; // Unit ID
request[7] = 0x10; // Function code (write multiple registers)
request[8] = startAddress >> 8;
request[9] = startAddress & 0xFF;
request[10] = regs2write >> 8;
request[11] = regs2write & 0xFF;
request[12] = byteCount;
//Load write buffer to transaction buffer
for (int i = 0; i < regs2write; ++i){
request[13 + i * 2] = tx_buffer[i] >> 8;
request[13 + i * 2 + 1] = tx_buffer[i] & 0xFF;
}
client.write(request, packetSize);
/*** Wait response with timeout ***/
//Get response
byte response[12]; //Expected response size
int index = 0;
while (client.available() && index < sizeof(response)){
response[index++] = client.read();
}
/*** Verify response data ***/
return true;
}Leer Holding Registers
Al contrario que en la escritura, esta función recoge en el buffer buff_rx el contenido solicitado. Se lee desde la dirección startAddress el número de registros indicados por regs2read. De nuevo se omiten las comprobaciones sobre la integridad de datos y transmisión.
bool readHoldingRegisters(WiFiClient& client, uint16_t startAddress, uint16_t* buff_rx, uint16_t regs2read){
/*** Check connection ***/
//Modbus TCP ReadHoldingReg packet
byte request[12] = {
0x00, 0x01, // Transaction ID
0x00, 0x00, // Protocol ID
0x00, 0x06, // Length
0x01, // Unit ID
0x03, // Function code (read holding registers)
(byte)(startAddress >> 8), (byte)(startAddress & 0xFF), // Start address
(byte)(regs2read >> 8), (byte)(regs2read & 0xFF) // Registers to read
};
client.write(request, sizeof(request));
/*** Wait response with timeout ***/
//Move response to aux buffer
int index = 0;
byte response[260]; // 125 registers (250 bytes + header)
while (client.available() && index < sizeof(response)) {
response[index++] = client.read();
}
/*** Verify response data ***/
//Get registers
for (int i = 0; i < regs2read; ++i) {
int offset = 9 + i * 2;
buff_rx[i] = (response[offset] << 8) | response[offset + 1];
}
return true;
}Depurar aplicación Modbus
Antes de enviar comandos directamente a un equipo conviene hacer pruebas en entornos controlados. Una forma muy recomendable para depurar Modbus es utilizar la aplicación ModRSsim2. Esta aplicación de Doug Lyons puede encontrarse en este enlace de Sourceforge, donde es posible descargar ejecutable y código fuente.

Una vez configurado es posible realizar operaciones de lectura/escritura contra la aplicación, ya sea desde una conexión TCP/IP como a través de un puerto serie tipo RS-232.
El siguiente código realiza una lectura de los diez primeros HoldingRegisters, incrementa su valor en una unidad y escribe el resultado en los mismos. Al ejecutar este código se verá desde ModRSsim2 como se incrementan estos registros automáticamente.
if (client.connect(fronius_ip, fronius_port)){
if(readHoldingRegisters(client, 0, registers, 10)){
for (int i = 0; i < 10; ++i) {
Serial.printf(" %04X", registers[i]);
}
Serial.println("");
for(int i = 0; i < 10; i++){
registers[i]++;
}
writeHoldingRegisters(client, 0, registers, 10);
}
}Como resultado:

Obtener precios de electricidad en España
La automatización que se plantea depende fundamentalmente del precio al que se encuentre la electricidad en un determinado momento. En España es posible obtener precios oficiales de electricidad desde Red Eléctrica de España (REE) y desde el Operador del Mercado Ibérico de la Electricidad (OMIE).
Mientras las webs proporcionan un entorno visual agradable para que un humano consulte información, las máquinas no necesitan tales decoraciones. Por ello existen protocolos de comunicación máquina-máquina (M2M), centrando el uso de recursos en transmitir únicamente datos.
Este canal de comunicación se suele denominar API, acrónimo de «interface de programación de aplicaciones». Las APIs suelen estar documentadas de forma que sean entendidas por un programador, en ocasiones incluyendo ejemplos de uso. En general es información de «especialistas» para «especialistas», y por ello será habitual tener que acabar realizando ensayo-error para conseguir algo.
API de REE y precios de la electricidad
La documentación de la API de REE se encuentra en este enlace. Esta documentación no incluye una descripción explícita de la información obtenida con cada consulta, teniendo que conocer de antemano la terminología del gremio.
La URL para obtener los precios diarios se forma de la siguiente manera:
https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real?start_date=YYYY-MM-DDT00:00&end_date=YYYY-MM-DDT23:59&time_trunc=hourDonde
- YYYY: Año a consultar, por ejemplo 2024.
- MM: Mes a consultar, por ejemplo 12.
- DD: Día a consultar, por ejemplo 31.
Con la fecha de ejemplo propuesta 31/12/2024 se construye la siguiente URL:
Fecha nada caprichosa, ya que es la última con precios negociados hora a hora. Desde el día siguiente y hasta ahora se utilizan intervalos de 15 minutos, como puede verse en la siguiente consulta.
Como anécdota, este cambio hizo fallar automatizaciones que dependían de esta API y no fueron preparadas para el cambio (incluyendo, como no, la que estás leyendo). Algunas automatizaciones tardaron en detectar este problema varios meses. La entrada de la primavera y la primera semana de precios negativos reveló el error, cuando el inversor seguía generando sin excepciones.
Consultar API de REE en Arduino
Construir la URL necesaria de forma dinámica en C puede hacerse de la siguiente manera.
#include <string.h>
char url_ree_api[255];
const char* url_ree_base = "https://apidatos.ree.es/";
const char* url_ree_precios = "es/datos/mercados/precios-mercados-tiempo-real?";
const char* url_ree_start_date = "start_date=";
const char* url_ree_end_date = "&end_date=";
const char* url_ree_timetrunc = "&time_trunc=hour";
sprintf(url_ree_api, "%s%s%s%04d-%02d-%02dT00:00%s%04d-%02d-%02dT23:59%s",
url_ree_base, url_ree_precios, url_ree_start_date,
year, month, day,
url_ree_end_date,
year, month, day,
url_ree_timetrunc);Con la URL lista se lanza la consulta a la API de REE desde Arduino:
#include <HTTPClient.h>
HTTPClient http;
http.begin(url_ree_api);
int http_res = http.GET();
if(http_res != 200){
//Other response than 200 means an error to be handled
Serial.print("HTTP Response: "); Serial.println(http_res);
}
//'json_in' gets the response request, including REE prices
String json_in = http.getString();
http.end();Procesar la respuesta es laborioso, manejar un JSON en lugar de datos crudos es caro para un microcontrolador. Seguramente muchas plataformas Arduino que soporten conexión TCP-IP serán capaces de guardar en RAM el string recibido, pero no es una situación deseable. Tras la inclusión de precios cada 15 minutos, el JSON puede llegar a 11kB para un día entero. Una posible solución sería consultar el precio en intervalos de pocas horas, o incluso solamente para las horas centrales del día.
Sea cual sea la estrategia, con el JSON en memoria toca extraer los precios. La librería <ArduinoJson.h> permite hacer esto de forma fácil para el programador (no tanto para el microcontrolador).
#include <ArduinoJson.h>
JsonDocument doc;
deserializeJson(doc, json_in);
JsonObject included_item = doc["included"][1];
JsonObject included_item_attributes = included_item["attributes"];
JsonArray json_spot_prices = included_item_attributes["values"].as<JsonArray>();
// The for loop only works with a full day record starting at 00:00h
for(size_t i = 0; i < 96; i++){
spot_price[i] = json_spot_prices[i]["value"];
}Integrando todo. Controlar un inversor mediante Modbus según precio de REE
Con todos los componentes funcionando no queda más que escribir una pequeña automatización. Esta comprueba si el precio de REE es negativo o positivo, deteniendo o reanudando la producción del inversor a través de Modbus.
updateTimeInfo();
float price_now = ree_today.get_price_at_time(timeinfo);
Serial.print(&timeinfo, "%d/%m/%Y-%H:%M:%S=>");
Serial.printf("Price: %f€, ", price_now);
if(price_now < 0){
Serial.printf("Stopping generation");
fronius.stopGeneration();
}else{
Serial.printf("Resuming generation");
fronius.resumeGeneration();
}
Serial.println("");Aunque es una aplicación simple, puede ser la semilla de proyectos mucho más grandes. Una vez se tiene acceso al inversor, es posible controlar cualquier aspecto de este. El criterio para ejecutar acciones depende únicamente de la imaginación de cada uno.
Por otro lado, conocer cómo hacer consultas mediante API desde un microcontrolador abre un mundo de posibilidades. Los sistemas embebidos ya no acaban donde llegan sus sensores, ahora pueden obtener información de cualquier lugar del mundo.
Controlar inversor con Modbus TCP, API REE y Arduino en GitHub
Los archivos de diseño de este proyecto se repositan «Open Source» en GitHub.

