This commit is contained in:
+6
-4
@@ -8,7 +8,7 @@ IP-Symcon-Registervariablen.
|
||||
## Funktionen
|
||||
|
||||
- positive Leistung bedeutet Laden, negative Leistung bedeutet Entladen
|
||||
- 250-W-Grundraster mit 50-W-Feinwerten zwischen -200 W und +200 W
|
||||
- stufenlose Leistungsbereiche in ganzen Watt innerhalb der dynamischen Grenzen
|
||||
- unterschiedliche Leistungsangebote fuer PV- und Peakbetrieb
|
||||
- Reserve- und Mindestladezustand mit konfigurierbarer Hysterese
|
||||
- direkte Registeransteuerung fuer herstellerunabhaengige Batterien, GoodWe,
|
||||
@@ -86,9 +86,11 @@ LadezustandHysterese ersetzt die bisher fest im Code hinterlegte
|
||||
|
||||
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
||||
|
||||
- Im PV-Betrieb steht oberhalb der Reserve das gesamte Raster zur Verfuegung.
|
||||
- Unterhalb der Reserve wird die maximale Ladeleistung angeboten.
|
||||
- Bei vollem Speicher werden nur Entladung und 0 angeboten.
|
||||
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
|
||||
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung.
|
||||
- Unterhalb der Reserve wird die maximale Ladeleistung als fester Wert angeboten.
|
||||
- Bei vollem Speicher wird der durchgaengige Bereich von der maximalen
|
||||
Entladeleistung bis 0 W angeboten.
|
||||
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
|
||||
Batterieleistung berechnete Entladewert angeboten.
|
||||
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
|
||||
|
||||
+10
-9
@@ -293,10 +293,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if ($sollleistung === null) {
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
&& !in_array(
|
||||
(int) $this->leseZustand('Sollleistung'),
|
||||
&& !Nachrichtenvertrag::enthaeltLeistungswert(
|
||||
$this->leseLeistungsangebot(),
|
||||
true
|
||||
(int) $this->leseZustand('Sollleistung')
|
||||
)
|
||||
) {
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
@@ -306,7 +305,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
||||
if (!Nachrichtenvertrag::enthaeltLeistungswert(
|
||||
$this->leseLeistungsangebot(),
|
||||
$sollleistung
|
||||
)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
||||
);
|
||||
@@ -394,10 +396,9 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
);
|
||||
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
&& !in_array(
|
||||
(int) $this->leseZustand('Sollleistung'),
|
||||
&& !Nachrichtenvertrag::enthaeltLeistungswert(
|
||||
$angebot,
|
||||
true
|
||||
(int) $this->leseZustand('Sollleistung')
|
||||
)
|
||||
) {
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
@@ -1073,7 +1074,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
/** @return list<int|array{Von_W: int, Bis_W: int}> */
|
||||
private function leseLeistungsangebot(): array
|
||||
{
|
||||
$angebot = json_decode(
|
||||
@@ -1086,7 +1087,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
return [0];
|
||||
}
|
||||
|
||||
return array_values(array_map('intval', $angebot));
|
||||
return array_values($angebot);
|
||||
}
|
||||
|
||||
private function setzeStoerung(string $text): void
|
||||
|
||||
@@ -29,8 +29,8 @@ Sollleistung_W verwendet folgende Semantik:
|
||||
- null: nur Betriebsart synchronisieren und neues Angebot anfordern
|
||||
|
||||
Ein konkreter Wert muss im zuletzt fuer dieselbe Betriebsart gemeldeten
|
||||
Leistungsangebot enthalten sein. Waehren der Aenderungssperre wird nur die
|
||||
Wiederholung des aktuellen Sollwerts akzeptiert.
|
||||
Einzelwert oder Leistungsbereich enthalten sein. Waehrend der Aenderungssperre
|
||||
wird nur die Wiederholung des aktuellen Sollwerts akzeptiert.
|
||||
|
||||
## Batterie an Manager
|
||||
|
||||
@@ -56,10 +56,10 @@ Beispiel:
|
||||
"EmpfaengerID": 10001,
|
||||
"Zeitpunkt": 1788825602
|
||||
},
|
||||
"Betriebsart": "Peak",
|
||||
"Betriebsart": "PV",
|
||||
"PrioritaetPV": 0,
|
||||
"PrioritaetPeak": 0,
|
||||
"Leistungswerte_W": [-1500],
|
||||
"Leistungswerte_W": [{"Von_W": -5000, "Bis_W": 5000}],
|
||||
"AenderungMoeglich": true,
|
||||
"Verfuegbar": true,
|
||||
"Istleistung_W": -250.0,
|
||||
@@ -68,7 +68,7 @@ Beispiel:
|
||||
{
|
||||
"Kennung": "Sollleistung_W",
|
||||
"Art": "Sollwert",
|
||||
"Wert": -1500,
|
||||
"Wert": -537,
|
||||
"Einheit": "W"
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,9 @@ Beispiel:
|
||||
|
||||
Ist die Leistungsmessung ungueltig, wird Istleistung_W als null und
|
||||
Leistungsquelle als 0 gemeldet. Bei einem gueltigen Messwert ist
|
||||
Leistungsquelle 2.
|
||||
Leistungsquelle 2. Frei regelbare Lade- und Entladeangebote werden als
|
||||
inklusive Bereiche in ganzen Watt gemeldet. Feste Schutz- und Peakvorgaben
|
||||
bleiben einzelne Leistungswerte.
|
||||
|
||||
## Ereignisse
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Verhaltensreferenz.
|
||||
- positive Sollleistung fuer Laden und negative Sollleistung fuer Entladen
|
||||
- dynamische maximale Lade- und Entladeleistung aus Variablen
|
||||
- Ladezustand, Netzleistung und aktuelle Batterieleistung als Eingaben
|
||||
- 250-W-Grundraster und 50-W-Feinwerte um 0
|
||||
- Reserve-, Mindestladezustands- und Hystereselogik
|
||||
- getrennte Angebote fuer PV und Peak
|
||||
- Wechselrichter- oder Enelix-Steuerung
|
||||
@@ -25,6 +24,7 @@ Verhaltensreferenz.
|
||||
| zyklischer Timer Do_UserCalc | Messwert- und Managerereignisse |
|
||||
| IdleCounter in Zyklen | Aenderungssperre in Sekunden mit Einmaltimer |
|
||||
| PowerSteps als Modulvariable | Leistungswerte_W direkt im Vertrag |
|
||||
| 250-W-Grundraster mit 50-W-Feinwerten | inklusive Leistungsbereiche in ganzen Watt |
|
||||
| interne Stellwertvariablen | ausgewaehlte Registervariablen mit RequestAction |
|
||||
| fest verdrahtete Herstellerhilfsvariablen | dynamisch sichtbare Registerauswahl |
|
||||
| fest codierte 2-%-Hysterese | Property LadezustandHysterese |
|
||||
|
||||
@@ -73,13 +73,17 @@ zuletzt Management geschrieben.
|
||||
|
||||
## Leistungsangebot
|
||||
|
||||
Das Raster besteht wie in Enelix 1 aus 250-W-Grundschritten. Um 0 werden
|
||||
zusaetzlich -200, -150, -100, -50, 0, 50, 100, 150 und 200 W angeboten,
|
||||
soweit diese Werte innerhalb der dynamischen Leistungsgrenzen liegen.
|
||||
Die Batterie meldet innerhalb der dynamischen Lade- und Entladegrenzen
|
||||
inklusive Leistungsbereiche in ganzen Watt. Der Manager kann dadurch jeden
|
||||
ganzzahligen Sollwert innerhalb des aktuell erlaubten Bereichs vorgeben. Die
|
||||
Grenzen werden abgerundet, damit keine dynamische Maximalleistung ueberschritten
|
||||
wird.
|
||||
|
||||
PV und Peak verwenden die bisherige SoC-Logik. Die Hysterese wird als
|
||||
persistenter Modulzustand gefuehrt. Bei Wechselrichtersteuerung, lokaler
|
||||
Deaktivierung oder ungueltigen Messwerten lautet das Angebot [0].
|
||||
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
|
||||
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
|
||||
gemeldet. Die Hysterese wird als persistenter Modulzustand gefuehrt. Bei
|
||||
Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen Messwerten
|
||||
lautet das Angebot [0].
|
||||
|
||||
## Variablen
|
||||
|
||||
@@ -115,7 +119,7 @@ Transport und gemeinsame Felder folgen
|
||||
|
||||
## Tests
|
||||
|
||||
- BatterieReglerTest: Raster, Hysterese, PV/Peak und Herstellerabbildung
|
||||
- BatterieReglerTest: Leistungsbereiche, Hysterese, PV/Peak und Herstellerabbildung
|
||||
- BatterieModulstrukturTest: Metadaten, Formular, Ereignismodell und Manager-ID
|
||||
- Symcon/modules/Batterie.php: reale Modulinstanz und Registeraktionen unter
|
||||
IP-Symcon 8.0
|
||||
|
||||
+23
-48
@@ -17,38 +17,6 @@ final class BatterieRegler
|
||||
public const STEUERUNG_WECHSELRICHTER = 1;
|
||||
public const STEUERUNG_ENELIX = 2;
|
||||
|
||||
/**
|
||||
* Erzeugt das bisherige Raster: 250-W-Grundschritte und
|
||||
* 50-W-Feinschritte im Bereich von -200 W bis +200 W.
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function leistungsraster(float $maxLadenW, float $maxEntladenW): array
|
||||
{
|
||||
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
|
||||
|
||||
$maxLaden = (int) floor($maxLadenW / 50.0) * 50;
|
||||
$maxEntladen = (int) floor($maxEntladenW / 50.0) * 50;
|
||||
$werte = [0];
|
||||
|
||||
for ($leistung = -$maxEntladen; $leistung <= 0; $leistung += 250) {
|
||||
$werte[] = $leistung;
|
||||
}
|
||||
for ($leistung = 0; $leistung <= $maxLaden; $leistung += 250) {
|
||||
$werte[] = $leistung;
|
||||
}
|
||||
for ($leistung = -200; $leistung <= 200; $leistung += 50) {
|
||||
if ($leistung >= -$maxEntladen && $leistung <= $maxLaden) {
|
||||
$werte[] = $leistung;
|
||||
}
|
||||
}
|
||||
|
||||
$werte = array_values(array_unique($werte));
|
||||
sort($werte, SORT_NUMERIC);
|
||||
|
||||
return $werte;
|
||||
}
|
||||
|
||||
public static function hystereseAktiv(
|
||||
float $ladezustand,
|
||||
float $reserve,
|
||||
@@ -68,9 +36,10 @@ final class BatterieRegler
|
||||
}
|
||||
|
||||
/**
|
||||
* Bildet das zustands- und betriebsartabhaengige Angebot aus Enelix 1 ab.
|
||||
* Bildet das zustands- und betriebsartabhaengige Angebot als
|
||||
* durchgaengige Leistungsbereiche in ganzen Watt ab.
|
||||
*
|
||||
* @return list<int>
|
||||
* @return list<int|array{Von_W: int, Bis_W: int}>
|
||||
*/
|
||||
public static function leistungsangebot(
|
||||
bool $aktiv,
|
||||
@@ -97,7 +66,8 @@ final class BatterieRegler
|
||||
return [0];
|
||||
}
|
||||
|
||||
$raster = self::leistungsraster($maxLadenW, $maxEntladenW);
|
||||
$maxLaden = (int) floor($maxLadenW);
|
||||
$maxEntladen = (int) floor($maxEntladenW);
|
||||
if ($peakbetrieb) {
|
||||
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||
$ziel = $netzleistungW > 0.0
|
||||
@@ -107,26 +77,26 @@ final class BatterieRegler
|
||||
return [self::begrenzeLeistung($ziel, $maxLadenW, $maxEntladenW)];
|
||||
}
|
||||
if ($ladezustand > $reserve && $hystereseAktiv) {
|
||||
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
||||
return self::leistungsbereich(-$maxEntladen, 0);
|
||||
}
|
||||
if ($ladezustand > $minimum) {
|
||||
return $raster;
|
||||
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||
}
|
||||
|
||||
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
||||
return self::leistungsbereich(0, $maxLaden);
|
||||
}
|
||||
|
||||
if ($ladezustand > 99.0) {
|
||||
return self::filtere($raster, static fn (int $wert): bool => $wert <= 0);
|
||||
return self::leistungsbereich(-$maxEntladen, 0);
|
||||
}
|
||||
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||
return $raster;
|
||||
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||
}
|
||||
if ($ladezustand >= $reserve && $hystereseAktiv) {
|
||||
return self::filtere($raster, static fn (int $wert): bool => $wert >= 0);
|
||||
return self::leistungsbereich(0, $maxLaden);
|
||||
}
|
||||
|
||||
return [(int) round($maxLadenW)];
|
||||
return [$maxLaden];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,12 +190,14 @@ final class BatterieRegler
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** @param list<int> $werte
|
||||
* @return list<int>
|
||||
*/
|
||||
private static function filtere(array $werte, callable $filter): array
|
||||
/** @return list<int|array{Von_W: int, Bis_W: int}> */
|
||||
private static function leistungsbereich(int $vonW, int $bisW): array
|
||||
{
|
||||
return array_values(array_filter($werte, $filter));
|
||||
if ($vonW === $bisW) {
|
||||
return [$vonW];
|
||||
}
|
||||
|
||||
return [['Von_W' => $vonW, 'Bis_W' => $bisW]];
|
||||
}
|
||||
|
||||
private static function begrenzeLeistung(
|
||||
@@ -233,7 +205,10 @@ final class BatterieRegler
|
||||
float $maxLadenW,
|
||||
float $maxEntladenW
|
||||
): int {
|
||||
return (int) round(min($maxLadenW, max(-$maxEntladenW, $leistungW)));
|
||||
$maxLaden = (int) floor($maxLadenW);
|
||||
$maxEntladen = (int) floor($maxEntladenW);
|
||||
|
||||
return (int) round(min($maxLaden, max(-$maxEntladen, $leistungW)));
|
||||
}
|
||||
|
||||
private static function pruefeLeistungsgrenzen(float $maxLadenW, float $maxEntladenW): void
|
||||
|
||||
@@ -70,6 +70,33 @@ final class Nachrichtenvertrag
|
||||
self::pruefeZustand($daten['Zustand']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||
*/
|
||||
public static function enthaeltLeistungswert(array $leistungswerte, int $leistung): bool
|
||||
{
|
||||
foreach ($leistungswerte as $eintrag) {
|
||||
if (is_int($eintrag)) {
|
||||
if ($eintrag === $leistung) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($eintrag)
|
||||
&& isset($eintrag['Von_W'], $eintrag['Bis_W'])
|
||||
&& is_int($eintrag['Von_W'])
|
||||
&& is_int($eintrag['Bis_W'])
|
||||
&& $leistung >= $eintrag['Von_W']
|
||||
&& $leistung <= $eintrag['Bis_W']
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $kopf
|
||||
*/
|
||||
|
||||
@@ -10,14 +10,14 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class BatterieReglerTest extends TestCase
|
||||
{
|
||||
public function testLeistungsrasterBehaeltGrobeUndFeineSchritte(): void
|
||||
public function testLeistungsbereichBleibtInnerhalbDynamischerGrenzen(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
-1000, -750, -500, -250, -200, -150, -100, -50,
|
||||
0, 50, 100, 150, 200, 250, 500, 750, 1000,
|
||||
],
|
||||
BatterieRegler::leistungsraster(1000.0, 1000.0)
|
||||
[['Von_W' => -999, 'Bis_W' => 1000]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 50.0, 20.0, 10.0, false,
|
||||
0.0, 0.0, 1000.9, 999.9
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,12 +28,10 @@ final class BatterieReglerTest extends TestCase
|
||||
self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true));
|
||||
}
|
||||
|
||||
public function testPvAngebotEntsprichtDemBisherigenSocVerhalten(): void
|
||||
public function testPvAngebotVerwendetStufenloseBereicheMitBisherigerSocLogik(): void
|
||||
{
|
||||
$voll = BatterieRegler::leistungsraster(1000.0, 1000.0);
|
||||
|
||||
self::assertSame(
|
||||
$voll,
|
||||
[['Von_W' => -1000, 'Bis_W' => 1000]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 50.0, 20.0, 10.0, false,
|
||||
0.0, 0.0, 1000.0, 1000.0
|
||||
@@ -47,7 +45,7 @@ final class BatterieReglerTest extends TestCase
|
||||
)
|
||||
);
|
||||
self::assertSame(
|
||||
[-1000, -750, -500, -250, -200, -150, -100, -50, 0],
|
||||
[['Von_W' => -1000, 'Bis_W' => 0]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 100.0, 20.0, 10.0, false,
|
||||
0.0, 0.0, 1000.0, 1000.0
|
||||
@@ -66,6 +64,24 @@ final class BatterieReglerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testPeakEinschraenkungenWerdenAlsBereicheGemeldet(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[['Von_W' => -1000, 'Bis_W' => 0]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, true, 21.0, 20.0, 10.0, true,
|
||||
0.0, 0.0, 1000.0, 1000.0
|
||||
)
|
||||
);
|
||||
self::assertSame(
|
||||
[['Von_W' => 0, 'Bis_W' => 1000]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, true, 10.0, 20.0, 10.0, true,
|
||||
0.0, 0.0, 1000.0, 1000.0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testWechselrichtersteuerungBietetNurNull(): void
|
||||
{
|
||||
self::assertSame(
|
||||
@@ -186,6 +202,9 @@ final class BatterieReglerTest extends TestCase
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
BatterieRegler::leistungsraster(-1.0, 1000.0);
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 50.0, 20.0, 10.0, false,
|
||||
0.0, 0.0, -1.0, 1000.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,17 @@ final class NachrichtenvertragTest extends TestCase
|
||||
self::assertTrue(true);
|
||||
}
|
||||
|
||||
public function testLeistungswertWirdInEinzelwertenUndBereichenGefunden(): void
|
||||
{
|
||||
$angebot = [0, ['Von_W' => 1000, 'Bis_W' => 2000], 3000];
|
||||
|
||||
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 0));
|
||||
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 1537));
|
||||
self::assertTrue(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2000));
|
||||
self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 999));
|
||||
self::assertFalse(Nachrichtenvertrag::enthaeltLeistungswert($angebot, 2500));
|
||||
}
|
||||
|
||||
public function testNegativePrioritaetWirdAbgelehnt(): void
|
||||
{
|
||||
$daten = $this->verbraucherdaten();
|
||||
|
||||
@@ -119,10 +119,10 @@ PHP
|
||||
): void {
|
||||
IPS_RequestAction($batterieID, 'Aktiv', true);
|
||||
$managerdatenSenden('PV', null);
|
||||
$managerdatenSenden('PV', 500);
|
||||
$managerdatenSenden('PV', 537);
|
||||
|
||||
$test->waitUntil(
|
||||
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
||||
static fn (): bool => (float) GetValue($leistungID) === 537.0
|
||||
&& (int) GetValue($modusID) === 11
|
||||
&& (int) GetValue($managementID) === 2
|
||||
);
|
||||
@@ -136,9 +136,9 @@ PHP
|
||||
$modusID,
|
||||
$leistungID
|
||||
): void {
|
||||
$managerdatenSenden('PV', -500);
|
||||
$managerdatenSenden('PV', -843);
|
||||
$test->waitUntil(
|
||||
static fn (): bool => (float) GetValue($leistungID) === 500.0
|
||||
static fn (): bool => (float) GetValue($leistungID) === 843.0
|
||||
&& (int) GetValue($modusID) === 12
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user