diff --git a/examples/Demoanlage/README.md b/examples/Demoanlage/README.md index 784a421..6c5b86c 100644 --- a/examples/Demoanlage/README.md +++ b/examples/Demoanlage/README.md @@ -9,6 +9,8 @@ Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symco - einen bidirektionalen Batteriespeicher (10 kWh, +/-3,5 kW), - eine technische Anlagentopologie mit Hybridwechselrichter, PV-Fläche und Batteriespeicher für die Prognoseanbindung, +- simulierte Istleistungs- und Leistungsbegrenzungsregister des Wechselrichters, +- eine aktive anlagenweite Einspeisebegrenzung auf 3.000 W mit 100 W Toleranz, - Prognosetelemetrie aus PV, Hausverbrauch, Netzleistung und Batterie-SOC im 60-Sekunden-Intervall, - simulierte PV-Erzeugung, variable Bewölkung, Tageslastgang, Zusatzlast, @@ -49,7 +51,12 @@ Südausrichtung. Der 10-kWh-Speicher nutzt den gemeinsamen Hybridwechselrichter; seine dynamischen Lade- und Entladegrenzen werden beim Bootstrap in die technischen Stammdaten übernommen. Bei aktiver Manager-Lizenz uebertraegt der Manager zusaetzlich PV-Leistung, Hausverbrauch, Netzleistung und Batterie-SOC -alle 60 Sekunden an den Enelix-Prognosedienst. +alle 60 Sekunden an den Enelix-Prognosedienst. Die Demo verwendet ein absolutes +Wechselrichter-Leistungsregister in Watt. Der Manager begrenzt die tatsächliche +PV-Produktion so, dass die Gesamtanlage höchstens 3.000 W einspeist; innerhalb +einer Toleranz von 100 W bleibt der zuletzt gesetzte Stellwert bestehen. Der +intelligente Netzfahrplan bleibt deaktiviert, solange dessen Backendfassung nur +als ausstehende Version bereitliegt. ## Bedienung @@ -60,7 +67,9 @@ eine ungeregelte Zusatzlast verändern. Bei aktivierten dynamischen Profilen entstehen reproduzierbare Morgen- und Abendspitzen, ziehende Bewölkung und ein Tagesgang der Außentemperatur. Der Manager verteilt den verfügbaren PV-Überschuss nach Priorität auf beide Boiler, Entfeuchter, Pufferspeicher und -Batterie. +Batterie. Bei hoher PV-Erzeugung ist die Abregelung an den technischen Variablen +`PV-Wechselrichter Istleistung` und `PV-Wechselrichter Leistungsgrenze` sowie an +der auf etwa 3.000 W begrenzten Netzeinspeisung sichtbar. `Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1`, `Boiler 2`, `Pufferspeicher` und `Batteriespeicher` öffnen direkt die jeweiligen @@ -76,6 +85,8 @@ Simulationszeit. - Die fünf Verbraucher müssen Status 102 besitzen; der Manager nach Lizenzaktivierung ebenfalls (vorher erwarteter Status 203). - Der Manager muss fünf Teilnehmer aus vier unterschiedlichen Modultypen melden. - Bei ausreichendem PV-Überschuss müssen thermische Verbraucher schalten und die Batterie laden. +- Bei höherem verbleibendem PV-Überschuss muss das Wechselrichterregister die PV-Leistung so begrenzen, dass ungefähr 3.000 W ins Netz eingespeist werden. +- Wechselrichter-Istleistung und Leistungsgrenze müssen in der technischen Kategorie vorhanden und im Manager zugeordnet sein. - Puffertemperatur, Außentemperatur und Batterie-Ladezustand müssen sich über den Tageslauf verändern. - Netzleistung, PV-Leistung, Hausverbrauch, EMS-Leistung, Einzelverbräuche, Speicherleistung, Energiesummen und Temperaturen werden archiviert. - Die native Energieverteilung muss acht Knoten enthalten. diff --git a/examples/Demoanlage/bootstrap.php b/examples/Demoanlage/bootstrap.php index 1e68780..701acf1 100644 --- a/examples/Demoanlage/bootstrap.php +++ b/examples/Demoanlage/bootstrap.php @@ -113,6 +113,8 @@ function enelixDemoInstall(): array $batteryManagement = enelixDemoEnsureVariable($technical, 'BatterieManagement', 'Batterie Managementregister', 1, '', 0, 340); $batteryMode = enelixDemoEnsureVariable($technical, 'BatterieModus', 'Batterie Modusregister', 1, '', 0, 350); $batteryRegisterPower = enelixDemoEnsureVariable($technical, 'BatterieRegisterLeistung', 'Batterie Leistungsregister', 2, 'ENELIX.Demo.PowerPositive', 0.0, 360); + $inverterActual = enelixDemoEnsureVariable($technical, 'WechselrichterIstleistung', 'PV-Wechselrichter Istleistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 370); + $inverterLimit = enelixDemoEnsureVariable($technical, 'WechselrichterLeistungsgrenze', 'PV-Wechselrichter Leistungsgrenze', 2, 'ENELIX.Demo.PowerPositive', max(0.0, (float) GetValue($peak)), 380); $chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 390); IPS_SetHidden($chartUpdated, true); @@ -136,10 +138,10 @@ function enelixDemoInstall(): array foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact, $buffer1, $buffer2] as $actorId) { IPS_SetVariableCustomAction($actorId, $actorScript); } - foreach ([$batteryManagement, $batteryMode, $batteryRegisterPower] as $registerId) { + foreach ([$batteryManagement, $batteryMode, $batteryRegisterPower, $inverterLimit] as $registerId) { IPS_SetVariableCustomAction($registerId, $registerScript); } - foreach ([$temp1, $temp2, $bufferTemp, $outsideTemp, $batteryMaxCharge, $batteryMaxDischarge, $batterySoc, $batteryActual] as $sensorId) { + foreach ([$temp1, $temp2, $bufferTemp, $outsideTemp, $batteryMaxCharge, $batteryMaxDischarge, $batterySoc, $batteryActual, $inverterActual] as $sensorId) { SetValue($sensorId, GetValue($sensorId)); } @@ -285,6 +287,9 @@ function enelixDemoInstall(): array 'ACNennleistung_kW' => $pvNennleistungKw, 'ACEinspeisegrenze_kW' => 0.0, 'ACBezugsgrenze_kW' => 0.0, + 'IstleistungVariableID' => $inverterActual, + 'BegrenzungVariableID' => $inverterLimit, + 'Begrenzungsart' => 'watt', ]]; $pvKonfiguration = [[ 'ID' => 'demo-pv-1', @@ -325,6 +330,10 @@ function enelixDemoInstall(): array 'VerbraucherTimeout' => 15, 'SollwertSolarladen' => 0.0, 'Umschaltdifferenz' => 0.0, + 'EinspeisebegrenzungAktiv' => true, + 'Einspeisegrenze' => 3000.0, + 'Einspeisetoleranz' => 100.0, + 'NetzfahrplanAktiv' => false, 'AnlagenWechselrichter' => json_encode( $wechselrichterKonfiguration, JSON_THROW_ON_ERROR @@ -476,7 +485,11 @@ function enelixDemoTick(string $source = 'Manual'): void $sun = ($hour >= 6.0 && $hour <= 18.0) ? max(0.0, sin(M_PI * ($hour - 6.0) / 12.0)) : 0.0; - $pv = $peak * $sun * (1.0 - ($environment['cloud'] * 0.0085)); + $pvAvailable = $peak * $sun * (1.0 - ($environment['cloud'] * 0.0085)); + $inverterActual = enelixDemoRequiredId($technical, 'WechselrichterIstleistung'); + $inverterLimit = enelixDemoRequiredId($technical, 'WechselrichterLeistungsgrenze'); + $pv = enelixDemoLimitPvPower($pvAvailable, (float) GetValue($inverterLimit)); + SetValue($inverterActual, round($pv, 1)); $contacts = [ 'boiler11' => enelixDemoRequiredId($technical, 'Boiler1Stufe1'), @@ -541,6 +554,11 @@ function enelixDemoTick(string $source = 'Manual'): void IPS_RequestAction($consumerId, 'Melden', true); } IPS_RequestAction($manager, 'Regeln', true); + $pv = enelixDemoLimitPvPower( + $pvAvailable, + (float) GetValue($inverterLimit) + ); + SetValue($inverterActual, round($pv, 1)); } $battery = $managerReady @@ -597,6 +615,11 @@ function enelixDemoTick(string $source = 'Manual'): void } } +function enelixDemoLimitPvPower(float $availablePower, float $limitPower): float +{ + return min(max(0.0, $availablePower), max(0.0, $limitPower)); +} + function enelixDemoRenderFlow(array $state): string { $format = static fn (float $value): string => number_format($value, 0, ',', "'"); diff --git a/tests/DemoanlageBootstrapTest.php b/tests/DemoanlageBootstrapTest.php index bb8ab86..568b9d8 100644 --- a/tests/DemoanlageBootstrapTest.php +++ b/tests/DemoanlageBootstrapTest.php @@ -123,6 +123,13 @@ final class DemoanlageBootstrapTest extends TestCase self::assertStringContainsString("'PrognoseHausverbrauchVariableID' => \$houseDisplay", $source); self::assertStringContainsString("'PrognoseSOCVariableID' => \$batterySoc", $source); self::assertStringContainsString("'PrognoseSendeintervall' => 60", $source); + self::assertStringContainsString("'IstleistungVariableID' => \$inverterActual", $source); + self::assertStringContainsString("'BegrenzungVariableID' => \$inverterLimit", $source); + self::assertStringContainsString("'Begrenzungsart' => 'watt'", $source); + self::assertStringContainsString("'EinspeisebegrenzungAktiv' => true", $source); + self::assertStringContainsString("'Einspeisegrenze' => 3000.0", $source); + self::assertStringContainsString("'Einspeisetoleranz' => 100.0", $source); + self::assertStringContainsString("'NetzfahrplanAktiv' => false", $source); self::assertStringContainsString("'demo-hybrid-1'", $source); self::assertStringContainsString("'demo-pv-1'", $source); self::assertStringContainsString("'demo-battery-1'", $source); @@ -191,6 +198,14 @@ final class DemoanlageBootstrapTest extends TestCase self::assertSame(0.0, enelixDemoBatteryRegisterPower(2, 0, 1500.0)); } + public function testWechselrichtergrenzeBegrenztNurDieTatsaechlichePvLeistung(): void + { + self::assertSame(3000.0, enelixDemoLimitPvPower(8000.0, 3000.0)); + self::assertSame(2500.0, enelixDemoLimitPvPower(2500.0, 3000.0)); + self::assertSame(0.0, enelixDemoLimitPvPower(-100.0, 3000.0)); + self::assertSame(0.0, enelixDemoLimitPvPower(8000.0, -100.0)); + } + public function testRollbackIstExplizitBestaetigungspflichtig(): void { $this->expectException(RuntimeException::class);