fix(manager): Regelung und Energiefluss korrigieren
Tests / test (push) Successful in 48s

This commit is contained in:
dh
2026-09-29 16:17:49 +00:00
parent cae14cf399
commit 393969594e
7 changed files with 345 additions and 25 deletions
+9
View File
@@ -742,6 +742,15 @@
"name": "EnergieflussAnzeigen", "name": "EnergieflussAnzeigen",
"caption": "Energiefluss unter dem Manager anzeigen", "caption": "Energiefluss unter dem Manager anzeigen",
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);" "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}
]
} }
] ]
}, },
+4
View File
@@ -173,6 +173,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false); $this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false); $this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false); $this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
$this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv'); $this->EnableAction('Aktiv');
@@ -431,6 +432,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID') if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
&& (int) $nachricht === self::VM_UPDATE && (int) $nachricht === self::VM_UPDATE
) { ) {
$this->aktualisiereEnergieflusswerte();
$this->regeln(false); $this->regeln(false);
} }
} }
@@ -613,6 +615,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'Daten' => $daten, 'Daten' => $daten,
]; ];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->aktualisiereEnergieflusswerte();
$this->regeln(false); $this->regeln(false);
} }
@@ -2121,6 +2124,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) { if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
$this->aktualisiereFunFacts($funFactsID); $this->aktualisiereFunFacts($funFactsID);
} }
$this->aktualisiereEnergieflusswerte();
} }
private function istNumerischeVariable(int $variableID): bool private function istNumerischeVariable(int $variableID): bool
+210 -9
View File
@@ -256,12 +256,73 @@ trait ManagerEnergieTrait
IPS_SetName($instanzID, 'Energiefluss'); IPS_SetName($instanzID, 'Energiefluss');
IPS_SetPosition($instanzID, 540); IPS_SetPosition($instanzID, 540);
IPS_SetHidden($instanzID, false); 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 = [ $knoten = [
$this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'), $this->energieflussKnoten($pvID, 'PV-Anlage', 'solar'),
$this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'), $this->energieflussKnoten($netzID, 'Stromnetz', 'grid'),
$this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'), $this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'),
$this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'),
]; ];
$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, 'Calculation', true);
IPS_SetProperty( IPS_SetProperty(
$instanzID, $instanzID,
@@ -271,26 +332,166 @@ trait ManagerEnergieTrait
IPS_SetProperty($instanzID, 'EnableLimits', false); IPS_SetProperty($instanzID, 'EnableLimits', false);
IPS_SetProperty($instanzID, 'Restrictions', '[]'); IPS_SetProperty($instanzID, 'Restrictions', '[]');
IPS_ApplyChanges($instanzID); IPS_ApplyChanges($instanzID);
$this->aktualisiereEnergieflusswerte($verbraucher);
return 'Der Energiefluss wurde erstellt oder aktualisiert.'; return 'Der Energiefluss wurde erstellt oder aktualisiert.';
} }
/** @return array<string, mixed> */ /** @return array<string, mixed> */
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 [ return [
'VariableConfigType' => 0, 'VariableConfigType' => 0,
'VariableID' => $this->GetIDForIdent($ident), 'VariableID' => $variableID,
'SecondaryVariableID' => 1, 'SecondaryVariableID' => 1,
'Name' => $name, 'Name' => $name,
'Type' => $typ, 'Type' => $typ,
'Grouped' => false, 'Grouped' => $gruppiert,
'CalculationType' => 0, 'CalculationType' => 0,
'InfoVariableID' => 1, 'InfoVariableID' => $infoVariableID,
'Invert' => false, '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<array{
* InstanzID: int,
* Name: string,
* Typ: string,
* Istleistung_W: float,
* Sollleistung_W: float
* }>
*/
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<array<string, mixed>>|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 private function erstelleEnergiediagramme(): string
{ {
$leistung = [ $leistung = [
+32 -14
View File
@@ -128,7 +128,8 @@ final class ManagerRegler
} }
$festwert = self::istFestwert($daten['Leistungswerte_W']); $festwert = self::istFestwert($daten['Leistungswerte_W']);
if ($festwert) { $festgehalten = !$daten['AenderungMoeglich'];
if ($festwert || $festgehalten) {
$nichtSteuerbar += $leistung; $nichtSteuerbar += $leistung;
} }
@@ -144,6 +145,8 @@ final class ManagerRegler
'Energiegruppe' => (int) round((float) $energie / 2000.0), 'Energiegruppe' => (int) round((float) $energie / 2000.0),
'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Leistungswerte_W' => $daten['Leistungswerte_W'],
'Festwert' => $festwert, 'Festwert' => $festwert,
'Festgehalten' => $festgehalten,
'Sollwert' => self::angenommenerSollwert($daten),
]; ];
} }
@@ -160,6 +163,7 @@ final class ManagerRegler
}); });
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
$positiveRichtung = $verteilbudget - $nichtSteuerbar >= 0;
$sollwerte = []; $sollwerte = [];
$ausgangssumme = 0; $ausgangssumme = 0;
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
@@ -169,26 +173,41 @@ final class ManagerRegler
); );
continue; 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; $sollwerte[$eintrag['InstanzID']] = $ausgangswert;
$ausgangssumme += $ausgangswert; $ausgangssumme += $ausgangswert;
} }
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme; $rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
if ($eintrag['Festwert']) { if ($eintrag['Festwert'] || $eintrag['Festgehalten']) {
continue; continue;
} }
$instanzID = $eintrag['InstanzID']; $instanzID = $eintrag['InstanzID'];
$ausgangswert = $sollwerte[$instanzID]; $ausgangswert = $sollwerte[$instanzID];
if ($rest >= 0) { if ($positiveRichtung) {
if ($rest <= 0) {
continue;
}
$neuerSollwert = self::groessterLeistungswertBis( $neuerSollwert = self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'], $eintrag['Leistungswerte_W'],
$ausgangswert + $rest $ausgangswert + $rest
); );
} else { } else {
if ($rest >= 0) {
continue;
}
$neuerSollwert = self::kleinsterLeistungswertAb( $neuerSollwert = self::kleinsterLeistungswertAb(
$eintrag['Leistungswerte_W'], $eintrag['Leistungswerte_W'],
$ausgangswert + $rest $ausgangswert + $rest
@@ -200,7 +219,7 @@ final class ManagerRegler
$zugeteilt = $nichtSteuerbar; $zugeteilt = $nichtSteuerbar;
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
if (!$eintrag['Festwert']) { if (!$eintrag['Festwert'] && !$eintrag['Festgehalten']) {
$zugeteilt += $sollwerte[$eintrag['InstanzID']]; $zugeteilt += $sollwerte[$eintrag['InstanzID']];
} }
} }
@@ -274,8 +293,10 @@ final class ManagerRegler
/** /**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte * @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/ */
private static function neutralerLeistungswert(array $leistungswerte): int private static function richtungsneutralerLeistungswert(
{ array $leistungswerte,
bool $positiveRichtung
): int {
$negativ = null; $negativ = null;
$positiv = null; $positiv = null;
foreach ($leistungswerte as $eintrag) { foreach ($leistungswerte as $eintrag) {
@@ -298,14 +319,11 @@ final class ManagerRegler
: $positiv; : $positiv;
} }
if ($negativ === null) { if ($positiveRichtung) {
return $positiv; return $positiv ?? $negativ;
}
if ($positiv === null) {
return $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 max($eintrag['Von_W'], $untergrenze);
} }
return self::neutralerLeistungswert($leistungswerte); return self::richtungsneutralerLeistungswert($leistungswerte, false);
} }
} }
@@ -19,6 +19,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)", "RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
"RegisterPropertyBoolean('FunFactsAnzeigen', false)", "RegisterPropertyBoolean('FunFactsAnzeigen', false)",
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)", "RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
"RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)",
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)", "RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)", "RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)", "RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
@@ -48,6 +49,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase
'FormEnergiediagrammeAnzeigen', 'FormEnergiediagrammeAnzeigen',
'FormFunFactsAnzeigen', 'FormFunFactsAnzeigen',
'FormEnergieflussAnzeigen', 'FormEnergieflussAnzeigen',
'EnergieflussLeistungseinheit',
'MesswerteAnzeigen', 'MesswerteAnzeigen',
'IstleistungVariableID', 'IstleistungVariableID',
'LeistungVariableID', 'LeistungVariableID',
+61
View File
@@ -258,6 +258,67 @@ final class ManagerReglerTest extends TestCase
self::assertSame(0, $ergebnis['Restabweichung_W']); 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 public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
{ {
$verbraucher = $this->verbraucher(10, 0, [3000], null, false); $verbraucher = $this->verbraucher(10, 0, [3000], null, false);
+27 -2
View File
@@ -268,6 +268,33 @@ return static function (TestContext $test): void {
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}', '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID'] 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); IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); $test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
@@ -416,12 +443,10 @@ return static function (TestContext $test): void {
SetValue($temperaturId, 53.0); SetValue($temperaturId, 53.0);
$test->waitUntil(static function () use ( $test->waitUntil(static function () use (
$managerId,
$warmwasserId, $warmwasserId,
$verbraucherstatus, $verbraucherstatus,
$zustandswert $zustandswert
): bool { ): bool {
IPS_RequestAction($managerId, 'Regeln', false);
$wert = $zustandswert( $wert = $zustandswert(
$verbraucherstatus(), $verbraucherstatus(),
$warmwasserId, $warmwasserId,