diff --git a/Manager/form.json b/Manager/form.json index 851eaea..e01761d 100644 --- a/Manager/form.json +++ b/Manager/form.json @@ -742,6 +742,15 @@ "name": "EnergieflussAnzeigen", "caption": "Energiefluss unter dem Manager anzeigen", "onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);" + }, + { + "type": "Select", + "name": "EnergieflussLeistungseinheit", + "caption": "Leistungseinheit im Energiefluss", + "options": [ + {"caption": "Watt (W)", "value": 0}, + {"caption": "Kilowatt (kW)", "value": 1} + ] } ] }, diff --git a/Manager/module.php b/Manager/module.php index 883be22..eb6e07a 100644 --- a/Manager/module.php +++ b/Manager/module.php @@ -173,6 +173,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle $this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false); $this->RegisterPropertyBoolean('FunFactsAnzeigen', false); $this->RegisterPropertyBoolean('EnergieflussAnzeigen', false); + $this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0); $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->EnableAction('Aktiv'); @@ -431,6 +432,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID') && (int) $nachricht === self::VM_UPDATE ) { + $this->aktualisiereEnergieflusswerte(); $this->regeln(false); } } @@ -613,6 +615,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle 'Daten' => $daten, ]; $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); + $this->aktualisiereEnergieflusswerte(); $this->regeln(false); } @@ -2121,6 +2124,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) { $this->aktualisiereFunFacts($funFactsID); } + $this->aktualisiereEnergieflusswerte(); } private function istNumerischeVariable(int $variableID): bool diff --git a/libs/ManagerEnergieTrait.php b/libs/ManagerEnergieTrait.php index 748cea0..d94b42b 100644 --- a/libs/ManagerEnergieTrait.php +++ b/libs/ManagerEnergieTrait.php @@ -256,12 +256,73 @@ trait ManagerEnergieTrait IPS_SetName($instanzID, 'Energiefluss'); IPS_SetPosition($instanzID, 540); IPS_SetHidden($instanzID, false); + + $profil = $this->energieflussLeistungsprofil(); + $pvID = $this->energieflussLeistungsvariable( + 'EnergieflussPV', + 'Energiefluss PV', + $profil, + 600 + ); + $netzID = $this->energieflussLeistungsvariable( + 'EnergieflussNetz', + 'Energiefluss Netz', + $profil, + 610 + ); + $hausID = $this->energieflussLeistungsvariable( + 'EnergieflussHaus', + 'Energiefluss Hausverbrauch', + $profil, + 620 + ); + $batterieID = $this->energieflussLeistungsvariable( + 'EnergieflussBatterie', + 'Energiefluss Batteriespeicher', + $profil, + 630 + ); + + $verbraucher = $this->energieflussVerbraucher(); $knoten = [ - $this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'), - $this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'), - $this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'), - $this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'), + $this->energieflussKnoten($pvID, 'PV-Anlage', 'solar'), + $this->energieflussKnoten($netzID, 'Stromnetz', 'grid'), + $this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'), ]; + $hatBatterieverbraucher = false; + $position = 640; + foreach ($verbraucher as $eintrag) { + $istID = $this->energieflussLeistungsvariable( + 'EnergieflussIst_' . $eintrag['InstanzID'], + $eintrag['Name'] . ' Istleistung', + $profil, + $position + ); + $sollID = $this->energieflussLeistungsvariable( + 'EnergieflussSoll_' . $eintrag['InstanzID'], + $eintrag['Name'] . ' Sollleistung', + $profil, + $position + 1 + ); + $knoten[] = $this->energieflussKnoten( + $istID, + $eintrag['Name'], + $eintrag['Typ'], + true, + $sollID + ); + $hatBatterieverbraucher = $hatBatterieverbraucher + || $eintrag['Typ'] === 'storage'; + $position += 10; + } + if (!$hatBatterieverbraucher) { + $knoten[] = $this->energieflussKnoten( + $batterieID, + 'Batteriespeicher', + 'storage' + ); + } + IPS_SetProperty($instanzID, 'Calculation', true); IPS_SetProperty( $instanzID, @@ -271,26 +332,166 @@ trait ManagerEnergieTrait IPS_SetProperty($instanzID, 'EnableLimits', false); IPS_SetProperty($instanzID, 'Restrictions', '[]'); IPS_ApplyChanges($instanzID); + $this->aktualisiereEnergieflusswerte($verbraucher); return 'Der Energiefluss wurde erstellt oder aktualisiert.'; } /** @return array */ - private function energieflussKnoten(string $ident, string $name, string $typ): array - { + private function energieflussKnoten( + int $variableID, + string $name, + string $typ, + bool $gruppiert = false, + int $infoVariableID = 1 + ): array { return [ 'VariableConfigType' => 0, - 'VariableID' => $this->GetIDForIdent($ident), + 'VariableID' => $variableID, 'SecondaryVariableID' => 1, 'Name' => $name, 'Type' => $typ, - 'Grouped' => false, + 'Grouped' => $gruppiert, 'CalculationType' => 0, - 'InfoVariableID' => 1, + 'InfoVariableID' => $infoVariableID, 'Invert' => false, ]; } + private function energieflussLeistungsprofil(): string + { + if ($this->ReadPropertyInteger('EnergieflussLeistungseinheit') !== 1) { + return '~Power'; + } + + $profil = 'Enelix.Power.kW'; + if (!IPS_VariableProfileExists($profil)) { + IPS_CreateVariableProfile($profil, 2); + IPS_SetVariableProfileValues($profil, -1000000.0, 1000000.0, 0.001); + IPS_SetVariableProfileDigits($profil, 3); + IPS_SetVariableProfileText($profil, '', ' kW'); + } + + return $profil; + } + + private function energieflussLeistungsvariable( + string $ident, + string $name, + string $profil, + int $position + ): int { + $this->RegisterVariableFloat($ident, $name, $profil, $position); + $variableID = $this->GetIDForIdent($ident); + IPS_SetHidden($variableID, true); + + return $variableID; + } + + /** + * @return list + */ + private function energieflussVerbraucher(): array + { + $cache = $this->leseJsonAttribut('VerbraucherCache'); + $ergebnis = []; + foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) { + if (!IPS_InstanceExists($instanzID)) { + continue; + } + $daten = $cache[(string) $instanzID]['Daten'] ?? null; + $istleistung = 0.0; + $sollleistung = 0.0; + if (is_array($daten)) { + $istleistung = $this->verbraucherleistung($daten); + $sollleistung = (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0); + } + $modulID = strtoupper( + (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] + ); + $katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? ''; + + $ergebnis[] = [ + 'InstanzID' => $instanzID, + 'Name' => IPS_GetName($instanzID), + 'Typ' => match ($katalog) { + 'battery' => 'storage', + 'ev_charger' => 'wallbox', + default => 'consumer', + }, + 'Istleistung_W' => $istleistung, + 'Sollleistung_W' => $sollleistung, + ]; + } + + return $ergebnis; + } + + /** + * @param list>|null $verbraucher + */ + private function aktualisiereEnergieflusswerte(?array $verbraucher = null): void + { + if (!$this->ReadPropertyBoolean('EnergieflussAnzeigen')) { + return; + } + $verbraucher = $verbraucher ?? $this->energieflussVerbraucher(); + $verbraucherleistung = 0.0; + $hatBatterieverbraucher = false; + foreach ($verbraucher as $eintrag) { + $this->setzeEnergieflussLeistungswert( + 'EnergieflussIst_' . $eintrag['InstanzID'], + (float) $eintrag['Istleistung_W'] + ); + $this->setzeEnergieflussLeistungswert( + 'EnergieflussSoll_' . $eintrag['InstanzID'], + (float) $eintrag['Sollleistung_W'] + ); + if ($eintrag['Typ'] === 'storage') { + $hatBatterieverbraucher = true; + } else { + $verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']); + } + } + + try { + $leistungen = $this->leseMessleistungen(); + } catch (Throwable $fehler) { + $this->protokolliere('Energiefluss', $fehler->getMessage()); + return; + } + $this->setzeEnergieflussLeistungswert('EnergieflussPV', $leistungen['PV']); + $this->setzeEnergieflussLeistungswert('EnergieflussNetz', $leistungen['Netz']); + $this->setzeEnergieflussLeistungswert( + 'EnergieflussHaus', + max(0.0, $leistungen['Haus'] - $verbraucherleistung) + ); + if (!$hatBatterieverbraucher) { + $this->setzeEnergieflussLeistungswert( + 'EnergieflussBatterie', + $leistungen['Batterie'] + ); + } + } + + private function setzeEnergieflussLeistungswert(string $ident, float $watt): void + { + $variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); + if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) { + return; + } + $faktor = $this->ReadPropertyInteger('EnergieflussLeistungseinheit') === 1 + ? 0.001 + : 1.0; + SetValue($variableID, $watt * $faktor); + } + private function erstelleEnergiediagramme(): string { $leistung = [ diff --git a/libs/ManagerRegler.php b/libs/ManagerRegler.php index 2a30139..c7aff6b 100644 --- a/libs/ManagerRegler.php +++ b/libs/ManagerRegler.php @@ -128,7 +128,8 @@ final class ManagerRegler } $festwert = self::istFestwert($daten['Leistungswerte_W']); - if ($festwert) { + $festgehalten = !$daten['AenderungMoeglich']; + if ($festwert || $festgehalten) { $nichtSteuerbar += $leistung; } @@ -144,6 +145,8 @@ final class ManagerRegler 'Energiegruppe' => (int) round((float) $energie / 2000.0), 'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Festwert' => $festwert, + 'Festgehalten' => $festgehalten, + 'Sollwert' => self::angenommenerSollwert($daten), ]; } @@ -160,6 +163,7 @@ final class ManagerRegler }); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung); + $positiveRichtung = $verteilbudget - $nichtSteuerbar >= 0; $sollwerte = []; $ausgangssumme = 0; foreach ($steuerbar as $eintrag) { @@ -169,26 +173,41 @@ final class ManagerRegler ); continue; } + if ($eintrag['Festgehalten']) { + if ($eintrag['Sollwert'] !== null) { + $sollwerte[$eintrag['InstanzID']] = $eintrag['Sollwert']; + } + continue; + } - $ausgangswert = self::neutralerLeistungswert($eintrag['Leistungswerte_W']); + $ausgangswert = self::richtungsneutralerLeistungswert( + $eintrag['Leistungswerte_W'], + $positiveRichtung + ); $sollwerte[$eintrag['InstanzID']] = $ausgangswert; $ausgangssumme += $ausgangswert; } $rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme; foreach ($steuerbar as $eintrag) { - if ($eintrag['Festwert']) { + if ($eintrag['Festwert'] || $eintrag['Festgehalten']) { continue; } $instanzID = $eintrag['InstanzID']; $ausgangswert = $sollwerte[$instanzID]; - if ($rest >= 0) { + if ($positiveRichtung) { + if ($rest <= 0) { + continue; + } $neuerSollwert = self::groessterLeistungswertBis( $eintrag['Leistungswerte_W'], $ausgangswert + $rest ); } else { + if ($rest >= 0) { + continue; + } $neuerSollwert = self::kleinsterLeistungswertAb( $eintrag['Leistungswerte_W'], $ausgangswert + $rest @@ -200,7 +219,7 @@ final class ManagerRegler $zugeteilt = $nichtSteuerbar; foreach ($steuerbar as $eintrag) { - if (!$eintrag['Festwert']) { + if (!$eintrag['Festwert'] && !$eintrag['Festgehalten']) { $zugeteilt += $sollwerte[$eintrag['InstanzID']]; } } @@ -274,8 +293,10 @@ final class ManagerRegler /** * @param list $leistungswerte */ - private static function neutralerLeistungswert(array $leistungswerte): int - { + private static function richtungsneutralerLeistungswert( + array $leistungswerte, + bool $positiveRichtung + ): int { $negativ = null; $positiv = null; foreach ($leistungswerte as $eintrag) { @@ -298,14 +319,11 @@ final class ManagerRegler : $positiv; } - if ($negativ === null) { - return $positiv; - } - if ($positiv === null) { - return $negativ; + if ($positiveRichtung) { + return $positiv ?? $negativ; } - return abs($negativ) <= abs($positiv) ? $negativ : $positiv; + return $negativ ?? $positiv; } /** @@ -361,6 +379,6 @@ final class ManagerRegler return max($eintrag['Von_W'], $untergrenze); } - return self::neutralerLeistungswert($leistungswerte); + return self::richtungsneutralerLeistungswert($leistungswerte, false); } } diff --git a/tests/ManagerEnergieModulstrukturTest.php b/tests/ManagerEnergieModulstrukturTest.php index 4c245e4..09d71b3 100644 --- a/tests/ManagerEnergieModulstrukturTest.php +++ b/tests/ManagerEnergieModulstrukturTest.php @@ -19,6 +19,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase "RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)", "RegisterPropertyBoolean('FunFactsAnzeigen', false)", "RegisterPropertyBoolean('EnergieflussAnzeigen', false)", + "RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)", "RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)", "RegisterPropertyInteger('LeistungLoeschenMonate', 12)", "RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)", @@ -48,6 +49,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase 'FormEnergiediagrammeAnzeigen', 'FormFunFactsAnzeigen', 'FormEnergieflussAnzeigen', + 'EnergieflussLeistungseinheit', 'MesswerteAnzeigen', 'IstleistungVariableID', 'LeistungVariableID', diff --git a/tests/ManagerReglerTest.php b/tests/ManagerReglerTest.php index 2ebacbb..31a3bc1 100644 --- a/tests/ManagerReglerTest.php +++ b/tests/ManagerReglerTest.php @@ -258,6 +258,67 @@ final class ManagerReglerTest extends TestCase self::assertSame(0, $ergebnis['Restabweichung_W']); } + public function testPvBetriebNutztBeiPositivemBudgetKeineNegativeLeistungsstufe(): void + { + $ergebnis = ManagerRegler::verteile( + 0.0, + 500.0, + [ + $this->verbraucher(10, 0, [-1000, 5000], 0), + $this->verbraucher(20, 1, [0, 3000], 0), + ], + 'PrioritaetPV' + ); + + self::assertSame(500, $ergebnis['Verteilbudget_W']); + self::assertSame([10 => 5000, 20 => 0], $ergebnis['Sollwerte']); + self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][10]); + self::assertSame(-4500, $ergebnis['Restabweichung_W']); + } + + public function testPvBetriebNutztNegativeLeistungErstBeiTatsaechlichemDefizit(): void + { + $ergebnis = ManagerRegler::verteile( + 1000.0, + 0.0, + [ + $this->verbraucher( + 10, + 0, + [['Von_W' => -5000, 'Bis_W' => 5000]], + 0 + ), + $this->verbraucher(20, 1, [0, 3000], 0), + ], + 'PrioritaetPV' + ); + + self::assertSame(-1000, $ergebnis['Verteilbudget_W']); + self::assertSame([10 => -1000, 20 => 0], $ergebnis['Sollwerte']); + self::assertSame(0, $ergebnis['Restabweichung_W']); + } + + public function testFestgehaltenerVerbraucherBilanziertIstUndBehaeltSollwert(): void + { + $festgehalten = $this->verbraucher(10, 0, [0, 3000, 5000], 2000, false); + $festgehalten['Daten']['Zustand'][0]['Wert'] = 3000; + + $ergebnis = ManagerRegler::verteile( + 0.0, + 3000.0, + [ + $festgehalten, + $this->verbraucher(20, 1, [['Von_W' => 0, 'Bis_W' => 5000]], 0), + ], + 'PrioritaetPV' + ); + + self::assertSame(5000, $ergebnis['Verteilbudget_W']); + self::assertSame(2000, $ergebnis['NichtSteuerbar_W']); + self::assertSame([10 => 3000, 20 => 3000], $ergebnis['Sollwerte']); + self::assertSame(0, $ergebnis['Restabweichung_W']); + } + public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void { $verbraucher = $this->verbraucher(10, 0, [3000], null, false); diff --git a/tests/Symcon/modules/Manager.php b/tests/Symcon/modules/Manager.php index ae6d5aa..1ff7cb3 100644 --- a/tests/Symcon/modules/Manager.php +++ b/tests/Symcon/modules/Manager.php @@ -268,6 +268,33 @@ return static function (TestContext $test): void { '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}', IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID'] ); + $knoten = json_decode( + (string) IPS_GetProperty($energieflussID, 'Config'), + true, + 512, + JSON_THROW_ON_ERROR + ); + $namen = array_column($knoten, 'Name'); + foreach (['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'] as $name) { + $test->assertTrue(in_array($name, $namen, true)); + } + foreach ($knoten as $knotenEintrag) { + if (in_array( + $knotenEintrag['Name'], + ['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'], + true + )) { + $test->assertTrue((int) $knotenEintrag['InfoVariableID'] > 1); + } + } + $pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId); + $test->assertSame(4200.0, (float) GetValue($pvFlussID)); + $test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']); + + IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1); + IPS_ApplyChanges($managerId); + $test->assertSame(4.2, (float) GetValue($pvFlussID)); + $test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']); IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false); $test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); @@ -416,12 +443,10 @@ return static function (TestContext $test): void { SetValue($temperaturId, 53.0); $test->waitUntil(static function () use ( - $managerId, $warmwasserId, $verbraucherstatus, $zustandswert ): bool { - IPS_RequestAction($managerId, 'Regeln', false); $wert = $zustandswert( $verbraucherstatus(), $warmwasserId,