Hier wie versprochen der Sketch. ( Geht nur bei Shelly 3EM Pro mit Modbus )
Eine Kinderkrankheit habe ich aber noch festgestellt.
Bei hohen Lastabfall geht der Growatt auf Fehler “Backflow Timeout” und startet die Einspeisung neu ( Connect in XX Sekunden ( Man hört auch das Relai klicken ).
Ich denke er meint damit das zu lange etwas ins Netz eingespeist wurde.
Ich habe versucht es zu kompensieren in dem ich im Menü den Prozentsatz für Export Limit von 0 auf 5% gesetzt habe. ( Bei kleinen Lastwechsel ( ~ 200 W ) funktioniert das Wunderbar ), bei größeren schafft er aber anscheinen nicht schnell genug runter zu regeln.
Habe ich den Wert auch mal auf 50…100% gesetzt, aber dann drückt er auch dauerhaft etwas zu viel in das Netz.
ggf. gibt auch in den Register eine Möglichkeit das Timeout höher zu setzen ?
Ansonsten wer noch eine Idee den hohen Lastabfall über den Sketch künstlich zu minimieren.
Zu den Einstellungen:
- Mein Growatt hat die Adresse 3
- Export Limit auf On ( 5% war in Test ganz Okay, bis auf hohe Lastwechel )
- Power Sensor hatte ich auf “Off” ( Was ist das überhaupt ? )
- Hatte MPPT aus und den Growatt an einer Batterie hängen ( Weiß nicht ob es relevant ist )
Im Sketch müsst ihr folgendes noch anpassen:
- Shelly IP Adresse
- Wifi Daten
Hardware:
ESP32
Max485 ( Flow controll )
Pins:
Growatt Mic Com stecker ( 7 ) => MAX485 ( A )
Growatt Mic Com stecker ( 8 ) => MAX485 ( B )
ESP32 ( 32 ) => Max485 ( RO )
ESP32 ( 33 ) => Max485 ( DI )
ESP32 ( 25 ) => Max485 ( DE + RE )
ESP32 ( 3.3V ) => MAX485 ( VCC )
ESP32 ( GND ) => Max485 ( GND )
Libary:
Sketch:
#include <WiFi.h>
#include <ModbusClientTCPasync.h>
// -----------------------------
// RS485 Pins zum Growatt (Meter-Emulation)
// -----------------------------
const int METER_RX_PIN = 32; // ESP32 RX2
const int METER_TX_PIN = 33; // ESP32 TX2
const int METER_DE_PIN = 25; // RE/DE
//Wifi zugangsdaten
char* ssid = "WIFI_SSID";
char* password = "GEHEIMES_PASSWORT";
//Modbus Shelly
IPAddress ip = {192, 168, 178, 71}; // IP vom Shelly Pro 3EM
uint16_t port = 502; // port of modbus server
int modbus_interval = 300;
static unsigned long Modbus_lastMillis = 0;
int modbus_fehler_count = 0;
// Create a ModbusTCP client instance
ModbusClientTCPasync MB(ip, port);
HardwareSerial SerialMeter(2); // UART2 für Growatt
//Div. globale Variablen
bool L1_error; //Stromklemme Error
bool L2_error; //Stromklemme Error
bool L3_error; //Stromklemme Error
bool N_error; //Stromklemme Error
bool S_error; //Phasen sequenz Error
float N_A; //Neutalleiter Strom
bool N_A_error; //Neutralleiter Nichtübereinstimmung
bool N_O_error; //Neutrallleiter Überstrom Error
float Total_A; //Strom Ingesamt
float Total_W; //Leistung Ingesamt
float Total_sch; //Scheinleistung Insgesamt
float L1_V; //Spannung der Phase
float L1_A; //Strom der Phase
float L1_W; //Leistung der Phase
float L1_SCH; //Scheinleistung der Phase
float L1_PF; //Powerfactor der Phase
bool L1_O_W_error; //Überleistung Error
bool L1_O_V_error; //Überspannung Error
bool L1_O_A_error; //Überstrom Error
float L1_Hz; //Freuqenz der Phase
float L2_V; //Spannung der Phase
float L2_A; //Strom der Phase
float L2_W; //Leistung der Phase
float L2_SCH; //Scheinleistung der Phase
float L2_PF; //Powerfactor der Phase
bool L2_O_W_error; //Überleistung Error
bool L2_O_V_error; //Überspannung Error
bool L2_O_A_error; //Überstrom Error
float L2_Hz; //Freuqenz der Phase
float L3_V; //Spannung der Phase
float L3_A; //Strom der Phase
float L3_W; //Leistung der Phase
float L3_SCH; //Scheinleistung der Phase
float L3_PF; //Powerfactor der Phase
bool L3_O_W_error; //Überleistung Error
bool L3_O_V_error; //Überspannung Error
bool L3_O_A_error; //Überstrom Error
float L3_Hz; //Freuqenz der Phase
// Antwort an Growatt bauen (stark vereinfacht, nur Grundwerte)
void sendDDSUReply(float voltage, float current, float power, float freq, float S_power, float PF) {
// DDSU666 liefert 16 Register (32 Byte Floatwerte)
// Growatt erwartet diese Struktur.
uint8_t frame[37];
frame[0] = 0x01; // Addr
frame[1] = 0x03; // Func
frame[2] = 0x20; // Start-High
frame[3] = 0x00; // Start-Low
frame[4] = 0x00; frame[5] = 0x10; // Quantity 16
// Prepare 16 registers (32 bytes) - map floats as:
// f0 = Voltage (V)
// f1 = Current (A) (computed = P/W / V)
// f2 = Active power (kW)
// f3 = Reserved (e.g. forward energy) -> 0
// f4 = Apparent power (kVA)
// f5 = Power factor
// f6 = Reserved = 0
// f7 = Frequency (Hz)
float S_kVA = S_power / 1000.0f;
float P_kW = power / 1000.0f;
float f[8];
f[0] = voltage;
f[1] = current;
f[2] = P_kW;
f[3] = 0.0f;
f[4] = S_kVA;
f[5] = PF;
f[6] = 0.0f;
f[7] = freq;
uint8_t resp[3 + 32]; // Slave, Func, ByteCount, 32 bytes data
resp[0] = 0x03;
resp[1] = 0x03;
resp[2] = 32; // 16 registers * 2 bytes
// fill data big-endian words hi,lo
int p = 3;
for (int i = 0; i < 8; i++) {
uint16_t hi, lo;
floatToRegpair(f[i], hi, lo);
resp[p++] = (hi >> 8) & 0xFF;
resp[p++] = hi & 0xFF;
resp[p++] = (lo >> 8) & 0xFF;
resp[p++] = lo & 0xFF;
}
sendRTUResponse(resp, sizeof(resp));
Serial.printf(">>> Sent GRID reply (0x2000): U=%.1fV I=%.3fA P=%.1fW F=%.2fHz\n", voltage, current, power, freq);
}
// convert float -> two registers (hi, lo) big-endian word order
void floatToRegpair(float f, uint16_t &hi, uint16_t &lo) {
uint32_t u;
memcpy(&u, &f, 4);
hi = (u >> 16) & 0xFFFF;
lo = u & 0xFFFF;
}
// ---------- Antwort senden ----------
void sendRTUResponse(uint8_t *payload, size_t payloadLen) {
// payload contains SlaveID, Func, ByteCount, data...
uint16_t crc = modbusCRC(payload, payloadLen);
uint8_t tail[2];
tail[0] = crc & 0xFF; // CRC low
tail[1] = (crc >> 8) & 0xFF;
// TX enable
digitalWrite(METER_DE_PIN, HIGH);
delayMicroseconds(100); // allow transceiver to switch
SerialMeter.write(payload, payloadLen);
SerialMeter.write(tail, 2);
SerialMeter.flush();
delayMicroseconds(100); // let last byte go
digitalWrite(METER_DE_PIN, LOW);
}
// ---------- Modbus RTU CRC ----------
uint16_t modbusCRC(const uint8_t *buf, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t pos = 0; pos < len; pos++) {
crc ^= (uint16_t)buf[pos];
for (int i = 0; i < 8; i++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
void setup() {
pinMode(METER_DE_PIN, OUTPUT);
digitalWrite(METER_DE_PIN, LOW);
Serial.begin(115200);
Serial.println("Booting");
SerialMeter.begin(9600, SERIAL_8N1, METER_RX_PIN, METER_TX_PIN);
// Connect to WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
delay(200);
Serial.println("Baue WiFi verbindung auf...");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(". ");
delay(1000);
}
IPAddress wIP = WiFi.localIP();
Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]);
// Set up ModbusTCP client.
// - provide onData handler function
MB.onDataHandler(&handleData);
// - provide onError handler function
MB.onErrorHandler(&handleError);
// Set message timeout to 2000ms and interval between requests to the same host to 200ms
MB.setTimeout(2000);
// Start ModbusTCP background task
MB.setIdleTimeout(60000);
MB.disconnect();
MB.connect(ip, 502);
}
void loop() {
if (millis() - Modbus_lastMillis > modbus_interval) {
Modbus_lastMillis = millis();
Error err;
//Modbus Anfage (Beliebiger Token der im Handler zur Anfrage indifizieren hilft, Modbus Geräte Adresse, Register (Input register), Register Start Adresse, Anzahl an register die ausgelesen werden )
err = MB.addRequest(Modbus_lastMillis, 1, 0x04, 1002, 77);
if (err != SUCCESS) {
ModbusError e(err);
Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e);
modbus_fehler_count = modbus_fehler_count + 1;
if(modbus_fehler_count > 5000){ ESP.restart(); }
}
}
}
//Modbus Daten Handler
// plus a user-supplied token to identify the causing request
void handleData(ModbusMessage response, uint32_t token)
{
int antwortzeit = millis() - token;
modbus_fehler_count = 0;
uint16_t offs = 3;
float floatValue = 0.0;
uint16_t boolValue;
/* //Hex ausgabe
for (auto& byte : response) {
Serial.printf("%02X ", byte);
}
*/
//Error's
offs = response.get(offs, boolValue); //31002
L1_error = boolValue;
offs = response.get(offs, boolValue); //31003
L2_error = boolValue;
offs = response.get(offs, boolValue); //31004
L3_error = boolValue;
offs = response.get(offs, boolValue); //31005
N_error = boolValue;
offs = response.get(offs, boolValue); //31006
S_error = boolValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31007-31008
N_A = floatValue;
offs = response.get(offs, boolValue); //31009
N_A_error = boolValue;
offs = response.get(offs, boolValue); //31010
N_O_error = boolValue;
//Total
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31011 - 12
Total_A = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31013 -14
Total_W = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31015 -16
Total_sch = floatValue;
//3 reserve registers
offs = response.get(offs, boolValue); //31017
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31018 - 19
//Phase A
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31020 - 21
L1_V = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31022 - 23
L1_A = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31024 - 25
L1_W = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31026 - 27
L1_SCH = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31028 - 29
L1_PF = floatValue;
offs = response.get(offs, boolValue); //31030
L1_O_W_error = boolValue;
offs = response.get(offs, boolValue); //31031
L1_O_V_error = boolValue;
offs = response.get(offs, boolValue); //31032
L1_O_A_error = boolValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31033 - 34
L1_Hz = floatValue;
//5 reserve registers
offs = response.get(offs, boolValue); //31035
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31036 - 37
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31038 - 39
//Phase B
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31040 - 41
L2_V = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31042 - 43
L2_A = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31044 - 45
L2_W = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31046 - 47
L2_SCH = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31048 - 49
L2_PF = floatValue;
offs = response.get(offs, boolValue); //31050
L2_O_W_error = boolValue;
offs = response.get(offs, boolValue); //31051
L2_O_V_error = boolValue;
offs = response.get(offs, boolValue); //31052
L2_O_A_error = boolValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31053 - 54
L2_Hz = floatValue;
//5 reserve registers
offs = response.get(offs, boolValue); //31055
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31056 - 57
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31058 - 59
//Phase C
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31060 - 61
L3_V = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31062 - 63
L3_A = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31064 - 65
L3_W = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31066 - 67
L3_SCH = floatValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31068 - 69
L3_PF = floatValue;
offs = response.get(offs, boolValue); //31070
L3_O_W_error = boolValue;
offs = response.get(offs, boolValue); //31071
L3_O_V_error = boolValue;
offs = response.get(offs, boolValue); //31072
L3_O_A_error = boolValue;
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31073 - 74
L3_Hz = floatValue;
//5 reserve registers
offs = response.get(offs, boolValue); //31055
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31056 - 57
offs = response.get(offs, floatValue, SWAP_REGISTERS); //31058 - 59
//An Growatt senden
sendDDSUReply(L1_V, Total_A, Total_W, L1_Hz, Total_sch, L1_PF);
}
//Error Handler für Modbus (Nicht weiter ausgeführt für das Projekt)
// Define an onError handler function to receive error responses
// Arguments are the error code returned and a user-supplied token to identify the causing request
void handleError(Error error, uint32_t token)
{
// ModbusError wraps the error code and provides a readable error message for it
ModbusError me(error);
Serial.printf("Error response: %02X - %s token: %d\n", (int)me, (const char *)me, token);
}
[ Nachtrag ]
Quick and Dirty Methode um auch bei großen Lastabfall nicht in den Fehler “Backflow Timeout” zu fallen.
Über:
sendDDSUReply(L1_V, Total_A, Total_W, L1_Hz, Total_sch, L1_PF);
Diese Zeile einfügen:
if(Total_W <= -50){ Total_W = -50; Total_A = 0.217; }
Sorgt dafür das der Growatt als Lastabfall nur noch Maximal -50Watt sieht.
( Export Limit im Growatt auf auf 1.0% Stellen, ggf erhöhen und Testen )