SolarEdge HybridWechselrichter mit Batterie steuern

Hallo Forum,

Teilweise schon hier diskutiert: ESS that can talk with OpenEMS using Modbus TCP, möchte ich dennoch ein neues Thema eröffnen. Es geht um die Steuerung eines hybriden System bestehend aus Solaredge-WR mit angeschlossener 48V-Batterie (ebenfalls Solaredge).
Eine Ansteuerung über die übliche applyPower()-Methode scheint nicht so einfach, da der Batterieteil kein Register hat um die Schein- und/oder Blindleistung zu setzen.
Solaredge selbst beschreibt den Vorgang so:

Initial configuration

  •  Set ExportConf_Ctrl (0xE000) to 0 to disable Export Configuration //This is used only when the inverter manages the power control. It is not applicable to Remote Control mode hence need to be disabled

  •  Set StorageConf_CtrlMode (0xE004) to 4 “Remote”

  •  Set StorageConf_AcChargePolicy (0xE005) to 1 “Always Allowed” // Applicable if AC charge is needed.

  •  StorageConf_AcChargeLimit (0xE006) //This is relevant only if StorageConf_AcChargePolicy (0xF705) is set to 2 or 3

  •  StorageConf_BackupReserved (0xE008) // only applicable to inverters that support backup hardware functionality

  •  Set StorageConf_DefaultMode (0xE00A) recommend to set to 1 “Charge excess PV” //default fallback mode in case of communication interruption.Use the following registers for dynamic commands:

  •  StorageRemoteCtrl_CommandTimeout (0xE00B): Sets the time duration in seconds for the new command (e.g. can be renewed at each write cycle for a few seconds).

  •  StorageRemoteCtrl_CommandMode (0xE00D): Sets the operating mode during the defined time frame according to the selected Storage Charge/Discharge Mode

  •  StorageRemoteCtrl_ChargeLimit (0xE00E): Battery charge power limit in watts up to the battery max power

  •  StorageRemoteCtrl_DischargeLimit (0xE010): Battery discharge power limit in watts up to the battery max power

Appendix B – Configuration Examples
NOTE:
This is effective only for exporting power to the AC. When importing power from the AC, the inverter power is defined by the Battery power limit and PV production (within the static maximum limits set for the Inverter).

Configurations examples for dynamic commands:

Discharge 1500W for 15 minutes:

  •  StorageRemoteCtrl_CommandTimeout = 900 the initial configuration after the time out.
  •  StorageRemoteCtrl_CommandMode = 4
  •  StorageRemoteCtrl_DischargeLimit = 1500Charge 2000W from the AC for 15 minutes:

Charge 2000W for 15 minutes:

  •  StorageRemoteCtrl_CommandTimeout = 900
  •  StorageRemoteCtrl_CommandMode = 3
  •  StorageRemoteCtrl_ChargeLimit = 2000

Also alles sehr umständlich. Verschiedene Modi müssen für Charge/Discharge-Änderungen umgeschaltet werden.
Bisher habe ich mich noch nicht getraut diese Operationen komplett in der applyPower()-Methode umzusetzen. Ein einfaches Umschalten zwischen Laden und Entladen scheint also nicht möglich. Wie ist eure Meinung dazu? Macht es Sinn alles in applyPower() zu “verpacken” oder sind das zu viele Schreibvorgänge in zu kurzer Zeit?
Vielleicht hat jemand etwas mehr Erfahrung zum Thema SolarEdge?

Bin für jeden Tipp danbkbar…

Gruß,
Klinki

Hi Kliniki,
es kommt oft vor, dass mehrere Register geschrieben werden müssen, um den Inverter zu konfigurieren.

Mehrfaches Modbus Schreiben kann in OpenEMS sehr effizient durch die Verwendung einer Verbindung erledigt werden.

Könntest du deine Code hier im Forum oder in einem Github Repository posten, damit wir ein Übersicht über deine bisherige Arbeiten bekommen?

Viele Grüße
Lóránt

Hallo Lóránt,

super Timing…gerade heute habe ich etwas daran experimentiert. Das Schreiben der Register funktioniert.

Meine applyPower-Methode sieht so aus:

	public void applyPower(int activePowerWanted, int reactivePowerWanted) throws OpenemsNamedException {
		CycleCounter++;
		
		// Read-only mode -> switch to max. self consumption automatic
		if (this.config.readOnlyMode() ) {
			if ( CycleCounter > 60) {
				CycleCounter=0;
				// Switch to automatic mode
				EnumWriteChannel setControlModeChannel = this.channel(SolarEdgeHybridEss.ChannelId.SET_CONTROL_MODE);
				setControlModeChannel.setNextWriteValue(ControlMode.SE_CTRL_MODE_MAX_SELF_CONSUMPTION);
				
				
				
				// The next 2 are fallback values which should become active after the 60 seonds timeout
				EnumWriteChannel setChargeDischargeDefaultMode	= this.channel(SolarEdgeHybridEss.ChannelId.SET_CHARGE_DISCHARGE_DEFAULT_MODE); //Same enum as Remote control mode
				setChargeDischargeDefaultMode.setNextWriteValue(ChargeDischargeMode.SE_CHARGE_POLICY_MAX_SELF_CONSUMPTION);	// This mode is active after remote control timeout exceeded
				
				IntegerWriteChannel setCommandTimeout		= this.channel(SolarEdgeHybridEss.ChannelId.SET_REMOTE_CONTROL_TIMEOUT);
				setCommandTimeout.setNextWriteValue(60); // Our Remote-commands are only valid for a minute
				
				setLimits();				
			}
			return;
		}
		else {
			if (CycleCounter > 0) { // Set values every Cycle
				CycleCounter=0;
				
				//read_only is NOT enabled
				EnumWriteChannel setControlMode 				= this.channel(SolarEdgeHybridEss.ChannelId.SET_CONTROL_MODE);
				EnumWriteChannel setChargePolicy 				= this.channel(SolarEdgeHybridEss.ChannelId.SET_STORAGE_CHARGE_POLICY);
				EnumWriteChannel setChargeDischargeDefaultMode	= this.channel(SolarEdgeHybridEss.ChannelId.SET_CHARGE_DISCHARGE_DEFAULT_MODE); //Same enum as Remote control mode
				EnumWriteChannel setChargeDischargeMode			= this.channel(SolarEdgeHybridEss.ChannelId.SET_REMOTE_CONTROL_COMMAND_MODE);	//Same enum as Remote control default mode
				
				
				IntegerWriteChannel setChargePowerLimit		= this.channel(SolarEdgeHybridEss.ChannelId.SET_MAX_CHARGE_POWER);
				IntegerWriteChannel setDischargePowerLimit	= this.channel(SolarEdgeHybridEss.ChannelId.SET_MAX_DISCHARGE_POWER);
				IntegerWriteChannel setCommandTimeout		= this.channel(SolarEdgeHybridEss.ChannelId.SET_REMOTE_CONTROL_TIMEOUT);
				
				
		
				if (isControlModeRemote() == false)
				{
					setControlMode.setNextWriteValue(ControlMode.SE_CTRL_MODE_REMOTE);	// Now the device can be remote controlled	
					setChargePolicy.setNextWriteValue(AcChargePolicy.SE_CHARGE_DISCHARGE_MODE_ALWAYS);	// Always allowed.When used with Maximize self-consumption, only excess power is used for charging (charging from the grid is not allowed) 
				}
	
	//			setControlMode.setNextWriteValue(ControlMode.SE_CTRL_MODE_REMOTE);	// Now the device can be remote controlled	
	//			setChargePolicy.setNextWriteValue(AcChargePolicy.SE_CHARGE_DISCHARGE_MODE_ALWAYS);	// Always allowed.When used with Maximize self-consumption, only excess power is used for charging (charging from the grid is not allowed) 
	
				
				// The next 2 are fallback values which should become active after the 60 seonds timeout
				setChargeDischargeDefaultMode.setNextWriteValue(ChargeDischargeMode.SE_CHARGE_POLICY_MAX_SELF_CONSUMPTION);	// This mode is active after remote control timeout exceeded
				setCommandTimeout.setNextWriteValue(60); // Our Remote-commands are only valid for a minute
				
				
				if (activePowerWanted <= 0) { // Negative Values are for charging
					setChargeDischargeMode.setNextWriteValue(ChargeDischargeMode.SE_CHARGE_POLICY_PV_AC); // Mode for charging
					setChargePowerLimit.setNextWriteValue(activePowerWanted * -1); // Values for register must be positive
				}
				else {
					setChargeDischargeMode.setNextWriteValue(ChargeDischargeMode.SE_CHARGE_POLICY_MAX_EXPORT); // Mode for discharging
					setDischargePowerLimit.setNextWriteValue(activePowerWanted); 
				}
			}
	
		}
		
	}

Der Modbus-Task schreibt alle Register auf Einmal:

		protocol.addTask(//
		new FC16WriteRegistersTask(0xE004, 
				m(SolarEdgeHybridEss.ChannelId.SET_CONTROL_MODE, new SignedWordElement(0xE004)),
				m(SolarEdgeHybridEss.ChannelId.SET_STORAGE_CHARGE_POLICY, new SignedWordElement(0xE005)), // Max. charge power. Negative values
				m(SolarEdgeHybridEss.ChannelId.SET_MAX_CHARGE_LIMIT, new FloatDoublewordElement(0xE006).wordOrder(WordOrder.LSWMSW)),  // kWh or percent
				m(SolarEdgeHybridEss.ChannelId.SET_STORAGE_BACKUP_LIMIT, new FloatDoublewordElement(0xE008).wordOrder(WordOrder.LSWMSW)),  // Percent of capacity 
				m(SolarEdgeHybridEss.ChannelId.SET_CHARGE_DISCHARGE_DEFAULT_MODE, new UnsignedWordElement(0xE00A)), // Usually set to 1 (Charge PV excess only)
				m(SolarEdgeHybridEss.ChannelId.SET_REMOTE_CONTROL_TIMEOUT, new UnsignedDoublewordElement(0xE00B).wordOrder(WordOrder.LSWMSW)),
				m(SolarEdgeHybridEss.ChannelId.SET_REMOTE_CONTROL_COMMAND_MODE, new UnsignedWordElement(0xE00D)),
				m(SolarEdgeHybridEss.ChannelId.SET_MAX_CHARGE_POWER, new FloatDoublewordElement(0xE00E).wordOrder(WordOrder.LSWMSW)),  // Max. charge power. Negative values
				m(SolarEdgeHybridEss.ChannelId.SET_MAX_DISCHARGE_POWER, new FloatDoublewordElement(0xE010).wordOrder(WordOrder.LSWMSW)) // Max. discharge power. Positive values
				)); // Disabled, automatic, remote controlled, etc.	

Kommentare und Programmierstil sind noch nicht so richtig schick…

Ich habe versuchsweise mal den Balancing-Controller für optimierten Eigenverbrauch aktiviert. Fehler beim Modbus-Write bekomme ich zwar nicht, aber die Anlage “überregelt” ziemlich heftig. Die Cycle-Time ist bei 1 Sekunde.

Auf GitHib ist der Code noch nicht - kommt in den nächsten Tagen…

Gruß,
Klinki

In Bezug auf die “Überregelung” habe ich festgestellt, dass ich mein System mehr oder weniger völlig falsch implementiert habe. Bisher waren die Geräte (PV-Inverter, ESS) getrennt, obwohl es eigentlich ein Gerät ist.
Stefan hatte das in diesem Thread schon mal beschrieben.
Aktuell bin ich dabei ein hybrides System zu programmieren und halte mich an die Vorlage der GoodWe-Implementierung. Diese ist für mich als Profi :roll_eyes: nicht so einfach zu durchblicken.
Was mir so ein bisschen fehlt ist eine Art Guideline welcher Channel von welchem nature mit welchem Wert gefüttert werden muss.

Gruß,
klinki

Ich bin jetzt doch etwas schlauer geworden. Nun habe ich Ess und die PV (DcCharger) Seite von einander getrennt. Als Vorlage diente der Commercial40 von Fenecon.

Im UI ist die Darstellung aber nicht korrekt:

Die Entladung im Widget passt, der Verbrauch ebenfalls. Nur der PV-Ertrag passt nicht.
Meine Frage zum Verständnis:

  • charger0.ActualPower → Aktuelle PV-Leistung DC
  • ess0.ActivePower → “Ausgabe”-Leistung des hybriden Wechselrichters AC
  • ess0.DcDischargePower → Leistung der Batterie DC

Stimmt das soweit?

Gruß
klinki

Problem war, bzw. ist, dass SolarEdge keine eigenen Register für die PV-Produktion bereitstellt. Der SunSpec-Channel DcW gibt die aktuelle Produktion des Wechselrichters raus. Diese enthält halt auch die Be-/Entladung der Batterie.
Letztere kommt bei meinem Setup vom Register “Battery Instantaneous Power”, also so etwas wie “DC Discharge Power”. Bei einem recht wolkenverhangenem Tag wie heute springen die Werte teilweise ziemlich hin und her.
Problematisch scheint hierbei auch zu sein, dass die Register teilweise über das SunSepc-Modul abgefragt sind und diese auch den Skalierungsfaktor beinhalten. Hier springt die Anzeige teilweise von 300W auf 30kW.
Den ActualPower -Channel berechne ich im EventTopic “TOPIC_CYCLE_AFTER_PROCESS_IMAGE” und nutze den SunSpec-Channel DcW, sowie den Solaredge-eigenen “Battery Instantaneous Power”.

Ist das sinnvoll oder sollte ein anderes Event genutzt werden?

Gruß,
Klinki

Der nächste Schritt ist nun das ESS auch zu steuern. Ich hatte mir dazu einen Balancing-Controller konfiguriert. Jetzt wird es interessant: In der Entwicklungsumgebung funktionert die Berechnung der Be-/Entladeleistung und das Setzen der Register im ESS tadellos.
Im Live-System aber leider nicht. Dort kommt es zu “Connection refused” Fehlern vom Wechselrichter wenn die entsprechenden Modbus-Register geschrieben werden sollen.
Die CycleTime ist bei beiden Systemen gleich. Natürlich habe ich auch versucht diese zu erhöhen (von 1Sek auf 10Sek). Aber auch dann meckert der Wechselrichter und nimmt die Modbus-Writes nicht an.

OpenEMS ist, neben dem was SolarEdge für seine App braucht, das einzige System welches auf den WR zugreift. Das verstehe ich nicht so recht.
Ich versuche gerade den Code dazu etwas “schlanker” zu gestalten und die Schreib-Befehle nur zu schicken wenn es auch nötig ist. Die entsprechende Statemachine würde wohl auch Sinn machen.

Das Live-System läuft auf einer recht perfomanten Hardware mit einem Debian-System. Das es beim Betriebssystem Einschränkungen gäbe wäre mir neu.

Hat vielleicht jemand ein ähnliches Problem?

Gruß
klinki

Hallo Forum,

Auch dieses Problem ist nun gelöst: Nach etwas Aufräum-Arbeit und vielen Versuchen mit möglichst effektivem Setzen von Modbus-Writes läuft es jetzt stabil.
Ein nicht unerhebliches Problem waren in der Tat die o.g. Skalierungseffekte bei SunSpec-Komponenten. Ihr könnt euch sicher vorstellen was mit dem Balancing-Controller passiert wenn die PV-Leistung von 300W auf 30kW springt…
Es blieb mir daher nichts anderes übrig als Active-Power für ESS und DcCharger zu berechnen.
Dies scheint aber ganz gut zu funktionieren.
Schade eigentlich, dass SolarEdge an dieser Stelle so schwierig ist. SunSpec ist an sich ja ein guter Ansatz.
Gruß,
klinki

Nachtrag: Der Balancing-Controller reagiert recht langsam und übersteuert gerne wenn nicht alle Channels zeitnah gefüttert werden.
Bisher habe ich die SunSpec-Umsetzung (mit Channel-Mapping) des Gridmeters genutzt. Hier ändern sich die Werte allerdings nur ca. alle 3 Zyklen. Für den Pid-Regler anscheinend zu langsam.
Also musste ich auch das Solaredge Gridmeter so umbauen, dass die Werte schneller kommen. Sprich: Die Register in einem separaten Modbus-Task abfragen und die ACTIVE_POWER selbst berechnen. Jetzt bekomme ich in jedem Zyklus andere Werte.
Über die Praxis kann ich noch nicht viel sagen: der Speicher ist aktuell voll :wink:

Guten Morgen Forum,

Es scheint jetzt alles prima zu funktionieren: die SunSpec-Skalierungs-Peaks sind nicht mehr aufgetreten und der Balancing-Controller kann nun schnell genug reagieren.
Das Ergebnis kann sich sehen lassen! Über Nacht hat der Speicher den Netzbezug nahezu bei 0 gehalten.
Man wird beobachten müssen wie sich das System bei schnell wechselnden Ereignissen verhält (dicke Wolken, etc.) und ggf. den Pid-Regler noch anpassen müssen.
Bisher reagiert das System aber augenscheinlich nicht schlechter als der SolarEdge eigene Hardware-Regler

Gruß,
klinki

1 Like

Hallo Klinki,

vielen Dank für deine Erfahrungsberichte! Das ist sehr interessant zu lesen und auch lehrreich für alle ‘neuen’. Ich kann nur bestätigen, dass es in der Praxis immer so ist, dass sich Geräte - seien es Wechselrichter, Batteriespeicher, Ladesäulen oder Wärmepumpen - im Detail anders verhalten, als man ursprünglich nach dem Lesen der Dokumentation und des (Modbus-)Protokolls gedacht hätte.

Ich verlinke hier der Vollständigkeit halber mal deinen Pull-Request mit dem zugehörigen Code:

Gruß,
Stefan

Moin Stefan,

Der Request ist schon etwas älter und weit hinter der aktuellen Entwicklung hinterher.
Ich schaffe es nicht ihn zu aktualisieren weil in Eclipse unter einem anderen Fork schon gleichnamige Pakages angelegt wurden. Ich lösche den alten request und mache einen neuen auf…

Gruß,
klinki

Hallo,

“Löschen” kann man einen Pull-Request nicht, sondern nur schließen.

Mit git push --force kannst du den bestehenden Branch einfach überschreiben. Dann kann der Pull-Request offen bleiben.

Mit SourceTree geht das auch grafisch: https://community.atlassian.com/t5/Sourcetree-questions/How-to-quot-force-quot-push/qaq-p/718539

Gruß,
Stefan

ich bin nicht sicher, ob ich und GitHub in diesem Leben noch Freunde werden…

Aber hier der aktuelle:
PullRequest

Hallo lieber Thomas und lieber Klinki

nochmals vielen Dank für euer Arbeit hier im Forum und im OpenEMS.
Ich habe einen Solaregde Wechselrichter SE10k Hybrid 48V mit BYD Storage System. Nun wollte ich das ganze mal von Git holen und ausprobieren. Leider scheint das ganze doch noch nicht in den Releases (2023-8) zu sein. Dann habe ich versucht das ganze vom develop und vom KlinkiFork zu laden. Leider gibts da eine Menge Fehlermeldungen (naja, ich muss ja zugegeben das ist ja auch unter Entwicklung). Meine Frage eher an Klinki, welche Version macht denn Sinn jetzt mal zu nehmen damit ich was lauffähiges habe? Bzw. ist da noch die Motivation da das ganze mal in ein OpenEms release zu bekommen?
Wie gesagt, vielen Dank nochmals für eure Arbeit hier !!!
lg
Ralph

Hi Ralph,

Du erwischt mich im besten Moment: Ich habe heute meinen alten Fork beerdigt und will auf die neue Version von openEMS wechseln. Es hat sich ja schon ne Menge getan in der Zwischenzeit…
Ich bin da aber schon recht weit. Es ist noch eine Frage von Tagen bis alles wieder läuft und der Fork wieder aktuell ist. Auf meinen PullRequest hatte noch niemand reagiert. Deswegen ist der auch noch nicht im aktuellen Repo. Die Geschichte läuft seit April stabil und sehr zufriedenstellend.
Es stehen aber noch Aufräumarbeiten im Code an. Falls Du Dich daran beteiligen willst: gerne!
Für ein Feedback bin ich aber auch sehr dankbar. Wie gesagt: hab ein paar Tage Geduld. Der WR passt ja, ich gehe davon aus, dass die Batterie ebenfalls “dumm” ist und mein Setup auch bei Dir passen könnte.

Gruß,
Klinki

PS: Thomas und Klinki ist die gleiche Person. Ich weiß auch nicht, warum ab und an “Thomas” und dann wieder “Klinki” da steht. Thomas Klinkenberg ist der Name :yum:

Guten Morgen,

Am Wochenende habe ich auf 2023.10 aktualisiert. Das Solaredge-Modul ist seither auch im Einsatz und scheint zu funktionieren. Wenn Du magst, kannst Du Dir das aus meinem Fork ziehen.
https://github.com/DerWahreKlinki/openemsFork

Seit gestern existiert auch ein PullRequest für das aktuelle develop-release. Es fehlt noch ein Review.
Über eine Rückmeldung würde ich mich freuen!

Gruß,
Klinki

Hallo Klinki
erst mal sorry für meine späte Antwort. Zuerst hatte ich Urlaub und dann hat mich die Arbeit in meiner Firma erschlagen. Vielen Dank für die Info und das update das du gemacht hast. ich hoffe ich komme in den nächsten Wochen dazu das mal auszuprobieren. Ich gebe da natürlich auch Rückmeldung. Ich bin jetzt zwar nicht der fitteste in Java, wenn ich es aber schaffe was im Code aufzuräumen, dann mache ich das natürlich gerne. Ich halte dich am laufenden und nochmals vielen Dank für deine Arbeit!!

Hi Ralph,

Du hast Dir einen guten Zeitpunkt ausgesucht: da bei SolarEdge einige Energie-Zähler (Charge/DisChargeEnergy) nicht vorhanden sind, nutzte ich bisher die CalculateEnergyFromPower-Klasse + ein paar Hilfs-Basteleien.

Problem war, dass die errechneten Zählerdaten keinen Neustart überstehen. Wohlgemerkt: nur wenn man Influx verwendet. Die entsprechende Methode den letzten Zählerstand aus der DB zu holen war einfach noch nicht umgesetzt.
Genau dieses habe ich in den letzten zwei Tagen getan. Seit gerade in meinem Fork.
Die Hilfs-Channels und -Funktionen habe ich aus dem Solaredge-Kram zunächst nur auskommentiert - was den Code noch besser lesbar macht :face_with_peeking_eye:

Heißt auch so rein praktisch: bis alle Änderungen in PullRequests verpackt und von der Community abgesegnet sind ist mein Fork relativ weit vom offiziellen Repo weg.
Falls Du mal Zeit finden solltest, meld Dich - ich kann Dir auch die exportiere .jar schicken.
Ehrlich gesagt bin ich auch recht neugierig ob mein Code auch bei anderen Installationen funktioniert.

Gruß,
klinki

Hallo Klinki
vielen Dank für deine Arbeit und dein Angebot. Ja evtl. kannst du mir das .jar zukommen lassen? Ich probiere das sehr gerne bei mir aus. hab auch die influx DB rennen.
lg
Ralph

Da fällt mir gerade ein, das .jar file bringt mir nicht so viel, weil da dann meine wärmepumpe fehlt. Ich glaube ich werde eher das aus deinem fork mal probieren.