Shelly Modbus zu Growatt Modbus ( SDM630 Emulation ) mit eine ESP aber ohne Umwege ( MQTT, Home Assistant e.t.c.)

Hallo zusammen,
Ich würde gerne den Growatt mic ein SDM630 Smartmeter vorgaukeln mit Hilfe eines ESP32.
Habe bereits auch schon viel gesucht, leider finde ich immer nur Umsetzungen über Home Assistant oder ähnliches. Meine begrenzten Englisch, Modbus und Home Assistant Kenntnisse erleichtern die ganze Sache dabei nicht.
Ich suche eigentlich ein simplen Beispiel Sketch wo ich dem Growatt mittels Variable einen Einspeisewert zuweisen kann.

Den Shelly per Modbus TCP auszulesen habe ich bereits hinbekommen, jedoch das Senden per Modbus macht mir immer noch starke Kopfschmerzen.

Ich bedanke mich schonmal Herzlich und hoffe das mir jemand unter die arme greifen kann.

Ein ähnliches Projekt habe ich bereits realisiert, jedoch Steuer ich damit einen Soyosource inverter ( RS485 ) an und keinen Modbus wie den Growatt, verstehe noch nicht ganz den unterschied.

Der Soyosource habe ich einfach mit einen Codesnipsel füttern können:
( Ich verstehe diesen Code jedoch nur Teilweise, was bewirkt z.b. “power >> 0x08” oder “power & 0xFF” )

int soyo_power_data[8] = {0x24, 0x56, 0x00, 0x21, 0x00, 0x00, 0x80, 0x08}; // 0 Watt



void setSoyoPowerData(int power){
  soyo_power_data[0] = 0x24;
  soyo_power_data[1] = 0x56;
  soyo_power_data[2] = 0x00;
  soyo_power_data[3] = 0x21;
  soyo_power_data[4] = power >> 0x08;
  soyo_power_data[5] = power & 0xFF;
  soyo_power_data[6] = 0x80;
  soyo_power_data[7] = calc_checksumme(soyo_power_data[1], soyo_power_data[2], soyo_power_data[3], soyo_power_data[4], soyo_power_data[5], soyo_power_data[6]);
}


int calc_checksumme(int b1, int b2, int b3, int b4, int b5, int b6 ){
  int calc = (0xFF - b1 - b2 - b3 - b4 - b5 -b6) % 256;
  return calc & 0xFF;
}



//LOOP
 if ((millis() - lastTime1) > timerDelay1) {
    
    if(value_power >=0){
      setSoyoPowerData(value_power);
      //events.send(String(value_power).c_str(),"staticpower", millis()); // nur an Website senden wenn es ein update vom Power_value gibt! neu 23.04.2023
      newData = false;
    }
    
    //send data to RS485 
    for(int i=0; i<8; i++){
      RS485Serial.write(soyo_power_data[i]);
      DEBUG_SERIAL.print(soyo_power_data[i], HEX);
      DEBUG_SERIAL.print(" ");
    }
}

Das Projekt habe ich euch soeben auch zur Verfügung gestellt falls Interesse besteht:
https://akkudoktor.net/t/soyosource-ueber-shelly-pro-3em-modbus-limitieren-abgeschlossen/

Falls jemand einen Codesnipsel hat um den Growatt einen SDM vorzugaukeln würde dies mir einiges an Lebenszeit ersparen.

Ist das einsam hier …

Habe heute mal mit den Growatt und einen USB zu RS485 Adapter rumgespielt. Das einzige was ich hinbekommen habe ist das er nicht mehr meckert das die Smartmeter Verbindung fehlt ( Error 401 ) und das er ganz wilde Sache macht bezüglich Leistung. Aber ihn dazu gebracht mal die Leistung auf einen eingestellten wert zu bringen, habe ich nicht geschafft.

Da nun über ein Tag dafür drauf gegangen sind, und die Unterstützung hier auch mehr als dürftig ist, wars das jetzt mit dem Growatt, der wird nun einfach ausgetauscht und fertig.

Ich habe die Idee, die Nulleinspeisung des Growatt via SDM-Emulation zu nutzen auch verworfen und drossele meinen Growatt über Growatt2MQTT direkt via eigener Node-Red-Funktion.

1 „Gefällt mir“

Doch etwas leben hier :heart:

Node-Red habe ich gehört, mich aber auch null mit beschäftigt. Dies ist auch Teil einen Smart Home System wozu extra ein Server gebraucht wird ?

Werde die Tage mich nochmal kurz mit den Growatt beschäftigen, hatte eher durch Zufall das Smart Meter DTSU666 gefunden, was wohl eher für den Growatt geeignet ist als das SDM Meter…

Das DTSU nutzt wohl auch andere Register als das SDM. Werde es also nochmal kurz mit den Registern vom DTSU testen müssen um weiterhin gut schlafen zu können :wink:

Kannst du mir sagen welche Register du mit dem Red-node fütterst ?

Ich habe mir als Speicher ein Victron-System aufgebaut. Da war Node-Red automatisch mit dabei und auch die Werte des EVU-Zählers. Da habe ich mich nach und nach eingefuchst und so einige Regeln mit umgesetzt.

Ich habe bei mir diese Version umgesetzt, die von einem User um eine Website erweitert wurde:

Ich sende an den ESP32 ein CURL-Befehl mit dem Prozentwert und der drosselt dann entsprechend.

regMaxOutputActive müsste dann auf Register 03 landen

// Growatt Holding registers
static const uint8_t regOnOff           = 0;
static const uint8_t regMaxOutputActive = 3;
static const uint8_t regStartVoltage    = 17;
static const uint8_t regModulPower      = 121;

Für meinen zweiten Growatt suche ich übrigens noch einen Com-Stecker…

1 „Gefällt mir“

Den hatte ich mir mal in die bookmarks gelegt, weil ich 2 growatts mit einem smartmeter versorgen wollte, aber bin noch zu nichts gekommen. Vll hilft es dir weiter bei deinem Problem.

1 „Gefällt mir“

@kpax vielen Dank, ist zwar auch wieder so ein mqtt Projekt, aber sehr einfach gehalten. Das könnte mir tatsächlich schon sehr weiterhelfen.

@elmo Danke, ist dieses Prozentuale Drossel über die Smart Meter Anschlüsse oder über die Kommunikation Schnittstelle wo man auch über die Register Einstellungen vornehmen kann ( wie Maximale Inverter Leistung ) ?

Ich werde wahrscheinlich erst Dienstag wieder Zeit dafür finden, aber bin etwas erschreckt darüber das ich anscheinend die falschen Register genutzt habe :smiley: bzw es wohl verschiedene gibt SDM, DTSU666. Ich meine es wahr register 3013 was ich die ganze Zeit gefüttert habe. Laut SDM modbus Datenblatt sollte dies so Stimmen. Aber na gut, nun habe ich weitere Infos zum Testen und bin wieder mit etwas Hoffnung gesegnet :wink:

Über die Smartmeter-Schnittstelle (den zweiten Modbus) bin ich nicht zum Erfolg gekommen. Ich hatte den Eindruck, dass der Growatt sich mit dem Leistungswert allein nicht zufrieden gibt.

Die prozentuale Drosselung habe ich über den ersten Modbus erreicht.

Zwischenupdate …..

Hab die letzten Tage viel Zeit darin investiert ein SPM630 mit den esp zu emulieren um den Mic zu drosseln leider trotz allen Bemühungen keine Change, also habe ich mich dazu entschlossen das ein echtes sdm630 her muss um die Kommunikation zu loggen.

Das sdm630 Modbus v2 soll laut diversen Bewertungen einwandfrei mit dem Growatt Mic600tl-x kommunizieren. Leider musste ich das Gegenteil feststellen. Der sdm zeigt zwar das Hörersymbol an ( Anfragen werden empfangen ) aber irgendwie interessiert den den Growatt die Antworten nicht. Hab auch diverse Einstellungen durchprobiert, keine Change.

Nun habe ich gelesen das ein spm-e ( einphasig ) wohl eine extra Growatt Version sein soll und besser funktionieren sollte. Ich schau also jetzt das ich so eins aufgetrieben bekomme.

Growatt habe ich ebenfalls anschreiben um etwas Licht ins dunkle zu bekommen, wieso einige berichten das ihr mic mit einem sdm630 v2 drosseln, meiner aber nicht. Evtl. gibt es ein Update für den Growatt, das meiner nicht bekommen hat. ( gh1.0/ghaa0750 )

Zwischenupdate 2.0 ( Ich habe ihn gebändigt ) ….

Ja ich mach mir immer noch Kopfschmerzen darüber, mit Hilfe von Chat GPT halbe ich mein Halbwissen über Modbus RTU etwas aufpoliert =D

Ich habe einen mitschnitt zwischen einen Growatt und einem DDSU666 bekommen, der mit den Growatt Mic werkelt.

Hier meine Notizen zum Log:

Auffällig war erst einmal, die Stromstärke. Anscheinen möchte der Growatt die Stromstärke * 2 gerechnet haben, ansonsten passen die Werte alle grob. ( Last war Primadaumen 100W ).

Daten sind im ABCD Float Format, zum umrechnen hilft diese Seite extrem:

Die Anfragen vom Growatt zum Meter sehen wie folgt aus:
03 03 20 00 00 10 4E 24 ( 20 = Fragt nach Meter Daten V, A, W, E-W, VA, Pf .. )
03 03 40 00 00 0C 51 ED ( 40 = Fragt nach Meter Daten Zählerstand Positive sowie Negative )

Jetzt muss man sich die Frage stellen, woher soll das Smartmeter wissen wieviel Last anliegt, wenn der Growatt gleichzeitig hinter dem Smartmeter Leistung reindrückt. Anders als wie z.b. beim Soyosource wird hinter den Smartmeter eingespeist und nicht davor, spricht Grundlast 150 Watt, Growatt = 150Watt, Fazit => Smartmeter misst ~ 0 Watt. Was soll das Smartmeter dem Growatt also interessantes mittteilen was er tun soll ?

Die Regelung geschieht über Negative / Positive werte, wobei er anscheinen nicht vergisst auf welcher Watt zahl er gerade geregelt hat ( Ich blick da selbst noch nicht zu 100% durch ).
Beim Anschließen regelt der Growatt solange hoch bis er einen Negativen wert sieht ( Einspeisung ), und kurz darauf wieder den eingestellten wert.
( Nach den Motto ich gehe mit der Einspeisung so lange hoch, bis ich sehe das etwas im Netz landet, warte dann darauf das es wieder auf Null ist oder Positiv ( Irgendwie auch nicht zu sehr Positiv ( Wird wahrscheinlich als Last hat sich erhöht gesehen. ) . Dann weiß ich, ich habe ich mein Ziel erreich und bleibe auf diesen Level / Last ).

Hoch geht es dann mit einen Positiven wert, der aber höher sein muss als der davor ( Er merkt sich anscheinend den alten Wert ), wenn man darauf dann wieder kurzzeitig einen Negativen werden sendet und darauf den Eingestellten Wert, hört er wieder auf hoch zu Regeln ( Auch wenn er den eingestellten wert noch nicht erreicht hat, für ihn zählt nur die aussage “Es ging kurz was in Netz” damit bin ich am Ziel ).

Runter geht es anscheinen immer mit einen Negativen Wert ( -0.030 scheint gut zu funktionieren),
Der Growatt regelt runter bis er wieder einen Wert von Null oder etwas Positiv sieht.
( Nach den Motto: Oh da geht was in Netz, ich drossel bis ich wieder was Positives oder 0 sehe )

So ganz blicke ich da immer noch nicht durch. Manchmal macht er komische Sachen ( Beim erneuten Hochregeln, springt er wahnsinnig schnell auf volle Einspeisung, das ich so schnell keinen Negativen wert hinterher senden kann, ggf. bin ich auch einfach mit der Hand zu langsam für den Growatt ).

Diese Regelung war für mich etwas verwirrend, macht aber bei einer Stecker Balkonkraftanlage richtig Sinn, da man den Stecker einfach irgendwo einstecken kann und nicht vor den Smartmeter einspeisen muss.

Das Smartmeter mit einen ESP zu Simulieren, um den Growatt einfach nur zu sagen, mach jetzt 100W und jetzt 150W wird also nicht ohne weiteres funktionieren, weil der ESP ohne weiteres nicht wissen kann wieviel der Growatt aktuell einspeist.
Da der Shelly auch Negative messen kann, sollte die Reglung Shelly => ESP => Growatt umzusetzen sein.

Jedoch möchte ich eigentlich weiterhin das mein Shelly die Last im Haus misst und nicht einfach nur 0 Watt loggt wenn die Sonne gut scheint.
Deshalb ist mein neuer Ansatz den Growatt zu entlocken wieviel Watt er gerade “Produziert”, damit der ESP den Growatt sagen kann “Hey zu viel”, “Hey zu wenig”, “Hey genau richtig, bleib so”.
Weißt jemand zufällig ob ich die aktuell Produzierte Leistung über die Meter Schnittstelle ( Pin 7, 8 ) auslesen kann ? Oder brauche ich dazu einen zweiten RS485 Adapter am ESP um an der Kommunikationsschnittstelle ( Pin 3,4 ) die aktuelle Leistung auszulesen ?

Wenn Du mit der Nulleinspeiseregelung über die Meterschnittstelle nicht zufrieden bist, wäre es da nicht einfacher, über Pin 3/4 den Leistungssollwert direkt vorzugeben. Da bekommst Du auch alle Werte ausgelesen.

Über die Meterschnittstelle fragt der Growatt nach meinem Verständnis nur den Einspeisezähler ab.

Das ist dann Plan B, falls ich mein Puzzel nicht gelöst bekomme :smiley:

Hast du zufällig eine Liste der Modbus register für den Growatt Mic ?

Bei mir läuft ein Growatt völlig sorgenfrei mit einem SDM630.

Ich hatte mir aber noch nicht dir Zeit genommen zu schauen was der Growatt auf der RS485 zum SDM macht.

Hast du einen Growatt Mic oder sph ? Beim Sdm630 gibt es wohl auch noch Unterschiede v1, v2, v3 und je nach ausgedruckter M Nummer soll es auch auch mal mehr oder weniger funktionieren. M24 soll wohl toll sein. Ich habe den v2 mit M25 Aufdruck gehabt und der wollte den Growatt wohl nicht in die Schranken weisen.

Ich habe einen SPH und der SDM ist V3. Zusätzlich hängt auch noch ein Growatt MIN mit dran

1 „Gefällt mir“

Das habe ich nach kurzer Suche im Nachbarforum gefunden.

Eingangsregister:

_address name description unit type len factor offset formula role room cw isScale

30109 RemoteCtrlEn uint16be 2 1 0 level false false

30110 RemoteCtrlPow uint16be 2 1 0 level false false

30120 Battery Type 0 Lead acid 1 Lithium battery uint16be 2 1 0 level false false

33001 Inverter Status 0:warten; 1:normal; 3:Fehler " " uint16be 1 1 0 level false false

33002 Ppv Eingangsleistung PV W uint32be 2 0.1 0 level false false

33004 Upv1 Spannung PV1 V uint16be 1 0.1 0 level false false

33005 Ipv1 Strom PV1 A uint16be 1 0.1 0 level false false

33006 Ppv1 Eingangsleistung PV1 W uint32be 2 0.1 0 level false false

33008 Upv2 Spannung PV2 V uint16be 1 0.1 0 level false false

33009 Ipv2 Strom PV2 A uint16be 1 0.1 0 level false false

33010 Ppv2 Eingangsleistung W uint32be 2 0.1 0 level false false

33024 Pac Ausgangsleistung AC W int32be 2 0.1 0 level false false

33026 Fac Ausgangsfrequenz Hz uint16be 2 0.01 0 level false false

33027 Vac1 Spannung L1 V uint16be 1 0.1 0 level false false

33028 Iac1 Strom L1 A uint16be 1 0.1 0 level false false

33029 Pac1 Leistung L1 VA uint32be 2 0.1 0 level false false

33031 Vac2 Spannung L2 V uint16be 1 0.1 0 level false false

33032 Iac2 Strom L2 A uint16be 1 0.1 0 level false false

33033 Pac2 Leistung L2 VA uint32be 2 0.1 0 level false false

33035 Vac3 Spannung L3 V uint16be 1 0.1 0 level false false

33036 Iac3 Strom L3 A uint16be 1 0.1 0 level false false

33037 Pac3 Leistung L3 VA uint32be 2 0.1 0 level false false

33039 Vac12 Spannung L1-L2 V uint16be 1 0.1 0 level false false

33040 Vac23 Spannung L2-L3 V uint16be 1 0.1 0 level false false

33041 Vac31 Spannung L3-L1 V uint16be 1 0.1 0 level false false

33042 Ptouser Ausgangsleistung Verbrauch AC W uint32be 2 0.1 0 level false false

33044 Ptogrid Ausgangsleistung Netz AC W uint32be 2 0.1 0 level false false

33046 Ptoload Ausgangsleistung Laden AC W uint32be 2 0.1 0 level false false

33048 Time_total Betriebszeit s uint32be 2 0.5 0 level false false

33050 Eac_today Wirklarbeit Heute kWh uint32be 2 0.1 0 level false false

33053 Eac_total Wirkarbeit gesamt kWh uint16be 2 0.1 0 level false false

33055 Epv_total Wirkarbeit PV gesamt kWh uint16be 2 0.1 0 level false false

33057 Epv1_today Wirkarbeit Heute PV1 kWh uint16be 2 0.1 0 level false false

33059 Epv1_total Wirkarbeit gesamt PV1 kWh uint16be 2 0.1 0 level false false

33061 Epv2_today Wirkarbeit Heute PV2 kWh uint16be 2 0.1 0 level false false

33063 Epv2_total Wirkarbeit gesamt PV2 kWh uint16be 2 0.1 0 level false false

33087 DeratingMode uint16be 1 1 0 level false false

33088 PVISO PVISOValue kOhm uint16be 1 1 0 level false false

33089 L1 DCI L1 DCI mA uint16be 1 0.1 0 level false false

33090 L3 DCI L2 DCI mA uint16be 1 0.1 0 level false false

33091 L3 DCI L3 DCI mA uint16be 1 0.1 0 level false false

33094 Temp1 Inverter IPM Temperatur °C uint16be 1 0.1 0 level false false

33095 Temp2 Inverter Temperatur °C uint16be 1 0.1 0 level false false

33096 Temp3 Boost Temperatur °C uint16be 1 0.1 0 level false false

33097 Temp4 Reserve Temperatur °C uint16be 1 0.1 0 level false false

33098 Temp5 Commmunication broad Temperatur °C uint16be 1 0.1 0 level false false

33099 P Bus Voltage P Bus interne Spannung V uint16be 1 0.1 0 level false false

33100 N Bus Voltage N Bus interne Spannung V uint16be 1 0.1 0 level false false

33101 IPF Inverter Ausgang PF uint16be 1 1 0 level false false

33102 RealOPPercent Ausgangsleistung % % uint16be 1 1 0 level false false

33104 OPFullwatt Ausgansleistung Limit max W uint16be 2 0.1 0 level false false

33106 Fault code Fehlercode uint16be 1 1 0 level false false

33107 Warn code Warnungscode uint16be 1 1 0 level false false

Holding Register:

_address name description unit type len factor offset formula role room poll wp cw isScale

40001 FernEinAus Ein aus über Fernsteuerung uint16be 1 1 0 level true false false false

40004 Active P Rate Inverter Max output active power percent % uint16be 1 1 0 level true false false false

40005 Reactve P Rate Inverter Max output reactive power percent % uint16be 1 1 0 level true false false false

40006 Power factor Inverter output power factor's 10000 times s uint16be 1 1 0 level true false false false

40008 Pmax Normal Power VA uint16be 2 0.1 0 level true false false false

40009 Vnormal Normal work PV voltage V uint16be 1 0.1 0 level true false false false

40049 Sys Std System Zeit Stunden std uint16be 1 1 0 level true false false false

40050 Sys Min System Zeit Sekunden min uint16be 1 1 0 level true false false false

40051 Sys Sec System Zeit Sekunden sec uint16be 1 1 0 level true false false false

40089 ModbusVersion ModbusVersion uint16be 1 1 0 level true false false false

40123 ExportLimit ExportLimit enabled uint16be 1 1 0 switch true false false false

40124 ExportLimitPowerrate ExportLimitPowerRate % uint16be 1 0.1 0 level true false false false

43001 ExportLimitFailedPowerRate Export Limit Failed PowerRate % uint16be 1 0.1 0 level true false false false

Ich hoffe du kannst damit was anfangen.

1 „Gefällt mir“

[ Letztes Update ]

Ich habe mich die Tage nochmals mit dem Growatt Limiter beschäftigt, ich verstehe die Steuerung irgendwie nicht so wirklich.

Sage ich ihm du ziehst gerade 10,20,30,…,80,90,100 watt aus dem Netz, interessiert es ihn überhaupt nicht. Sage ich ihm aber du ziehst 110watt aus dem Netz fängt er an hoch zu regeln bis ich ihn sage du schiebst etwas ins Netz und nun bist du wieder auf 0watt. ( Darauf bleibt er stehen )

Mit -30 Watt kann man ihn wieder runter regeln lassen bis man ihn sagt nun sind es Null Watt.

Hin und wieder beim testen ihn hoch und runter zu regeln, steigt er aber dann komplett aus und lässt sich überhaupt nicht mehr bändigen.

Ich vermute fast das die aufgerufene Leistung über die rs485 Kommunikation Schnittstelle nicht immer super Aktuell ist und die Regelung deswegen versagt.

Ich bin also zu Plan B und hab ihn über seine Gesamtleistung limitiert wie einige es schon machen. Dies Funktioniert auch super einfach, hat aber wie ich gemerkt habe den Nachteil, daß er Hoch und runterregelt wie eine Oma. Und ob es auf dauert so gesund die die Register permanent zu beschreiben ( Schreib / Lesezyklen ) hat mir auch etwas Kopfschmerzen gemacht.

Bevor ich alles im Keim ersticke, habe ich mich nochmal an die Idee gemacht den Shelly einfach in ein DDSU666 Smartmeter zu wandeln ( Shelly per Modbus auslesen und Werte in das DDSU666 Format wandeln und letztendlich an die Smartmeter Schnittstelle von Growatt zu senden ). Und siehe da, merkwürdiger weise regelt der Growatt wie mit einem echten DDSU666, regelt Schnell hoch und Runter, und versucht ständig den Shelly auf +/- 0 Watt zu bekommen.

Ich verstehe nicht was an den Werten vom Shelly anders ist, als die Werte die ich ihn vorgegaukelt habe, vielleicht fehlt ihm da ein wenig Dynamik ( Last ändert sich zu selten, Ausgelesene Leistung vom Growatt entspricht nicht ganz den Aktuellen Wert, oder oder oder .. )

Ich lege das Projekt jedenfalls erst einmal bei Seite.

Das Shelly zu DDSU666 Skript für den Esp werde ich noch ein wenig aufräumen / übersichtlicher machen, und es euch die Tage hier zur Verfügung stellen.

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 )

Okay vergisst den Quick and Dirty Fix,
Growatt Mic tl-x Register 314 ( Super Anti backflow ) auf 0 für Aus.

Dann sind die Probleme mit Backflow nicht mehr vorhanden.

Schließe dann auch hier ab, komme mir wieder vor als führe ich Selbstgespräche.