feat: Batteriemodul ereignisbasiert integrieren
Tests / test (push) Successful in 45s

This commit is contained in:
dh
2026-09-22 09:27:49 +00:00
parent d03a7fd8fb
commit d7308663da
18 changed files with 2670 additions and 51 deletions
+106
View File
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use PHPUnit\Framework\TestCase;
final class BatterieModulstrukturTest extends TestCase
{
private const MODULPFAD = __DIR__ . '/../Batterie/';
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
{
foreach (['module.json', 'form.json', 'module.php', 'README.md'] as $datei) {
self::assertFileExists(self::MODULPFAD . $datei);
}
$metadaten = json_decode(
(string) file_get_contents(self::MODULPFAD . 'module.json'),
true,
512,
JSON_THROW_ON_ERROR
);
self::assertSame('{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}', $metadaten['id']);
self::assertSame('Batterie', $metadaten['name']);
self::assertSame('Enelix', $metadaten['vendor']);
self::assertSame(3, $metadaten['type']);
}
public function testAllePropertiesSindImFormularEnthalten(): void
{
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
preg_match_all(
'/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/',
$basis . $modul,
$treffer
);
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
foreach (array_values(array_unique($treffer[1])) as $property) {
self::assertStringContainsString(
'"name": "' . $property . '"',
$formular,
$property
);
}
}
public function testModulIstEreignisbasiertUndVerwendetVertrag40(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
self::assertStringContainsString('public function MessageSink(', $modul);
self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul);
self::assertStringContainsString("'Betriebsart' =>", $modul);
self::assertStringNotContainsString('Timer_Do_UserCalc_Battery', $modul);
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
}
public function testRegisterWerdenUeberAusgewaehlteAktionsvariablenGeschrieben(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
self::assertStringContainsString(
'RequestAction($this->ReadPropertyInteger($property), $wert)',
$modul
);
self::assertStringContainsString('FormBatterietyp', $modul);
self::assertStringContainsString('UpdateFormField', $modul);
foreach ([
'ManagementRegisterVariableID',
'ModusRegisterVariableID',
'LeistungsRegisterVariableID',
'LadeleistungRegisterVariableID',
'EntladeleistungRegisterVariableID',
] as $property) {
self::assertStringContainsString('"name": "' . $property . '"', $formular);
}
}
public function testLoggingDiagnoseUndAktionsbuttonsSindVorhanden(): void
{
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
self::assertStringContainsString('"name": "DiagnosevariablenAnzeigen"', $formular);
self::assertStringContainsString('Messwerte und Angebot aktualisieren', $formular);
self::assertStringContainsString('Register auf sicheren Zustand setzen', $formular);
}
public function testManagerKenntDieBatterieModulIDGenauEinmal(): void
{
$manager = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
self::assertSame(
1,
substr_count($manager, '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}')
);
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use Belevo\EnelixEMS\BatterieRegler;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
final class BatterieReglerTest extends TestCase
{
public function testLeistungsrasterBehaeltGrobeUndFeineSchritte(): 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)
);
}
public function testHystereseSchaltetAnDenBisherigenGrenzen(): void
{
self::assertTrue(BatterieRegler::hystereseAktiv(20.0, 20.0, 2.0, false));
self::assertTrue(BatterieRegler::hystereseAktiv(21.0, 20.0, 2.0, true));
self::assertFalse(BatterieRegler::hystereseAktiv(22.1, 20.0, 2.0, true));
}
public function testPvAngebotEntsprichtDemBisherigenSocVerhalten(): void
{
$voll = BatterieRegler::leistungsraster(1000.0, 1000.0);
self::assertSame(
$voll,
BatterieRegler::leistungsangebot(
true, true, false, 50.0, 20.0, 10.0, false,
0.0, 0.0, 1000.0, 1000.0
)
);
self::assertSame(
[1000],
BatterieRegler::leistungsangebot(
true, true, false, 19.0, 20.0, 10.0, true,
0.0, 0.0, 1000.0, 1000.0
)
);
self::assertSame(
[-1000, -750, -500, -250, -200, -150, -100, -50, 0],
BatterieRegler::leistungsangebot(
true, true, false, 100.0, 20.0, 10.0, false,
0.0, 0.0, 1000.0, 1000.0
)
);
}
public function testPeakAngebotBeruecksichtigtNetzUndAktuelleBatterieleistung(): void
{
self::assertSame(
[-500],
BatterieRegler::leistungsangebot(
true, true, true, 50.0, 20.0, 10.0, false,
700.0, 200.0, 1000.0, 1000.0
)
);
}
public function testWechselrichtersteuerungBietetNurNull(): void
{
self::assertSame(
[0],
BatterieRegler::leistungsangebot(
true, false, false, 50.0, 20.0, 10.0, false,
0.0, 0.0, 1000.0, 1000.0
)
);
}
public function testHerstellerunabhaengigeRegisterwerte(): void
{
self::assertSame(
[
'Ladeleistung' => 1500,
'Entladeleistung' => 0,
'Modus' => 0,
'Management' => 1,
],
BatterieRegler::registerwerte(
BatterieRegler::TYP_HERSTELLERUNABHAENGIG,
BatterieRegler::STEUERUNG_ENELIX,
1500
)
);
self::assertSame(
[
'Ladeleistung' => 0,
'Entladeleistung' => 1500,
'Modus' => 1,
'Management' => 1,
],
BatterieRegler::registerwerte(
BatterieRegler::TYP_HERSTELLERUNABHAENGIG,
BatterieRegler::STEUERUNG_ENELIX,
-1500
)
);
}
public function testGoodweRegisterwerte(): void
{
self::assertSame(
['Leistung' => 750, 'Modus' => 11, 'Management' => 2],
BatterieRegler::registerwerte(
BatterieRegler::TYP_GOODWE,
BatterieRegler::STEUERUNG_ENELIX,
750
)
);
self::assertSame(
['Leistung' => 750, 'Modus' => 12, 'Management' => 2],
BatterieRegler::registerwerte(
BatterieRegler::TYP_GOODWE,
BatterieRegler::STEUERUNG_ENELIX,
-750
)
);
self::assertSame(
['Leistung' => 0, 'Management' => 1],
BatterieRegler::registerwerte(
BatterieRegler::TYP_GOODWE,
BatterieRegler::STEUERUNG_WECHSELRICHTER,
-750
)
);
}
public function testSolarEdgeRegisterwerte(): void
{
self::assertSame(
[
'Ladeleistung' => 0,
'Entladeleistung' => 500,
'Modus' => 4,
'Management' => 4,
],
BatterieRegler::registerwerte(
BatterieRegler::TYP_SOLAREDGE,
BatterieRegler::STEUERUNG_ENELIX,
-500
)
);
}
public function testSigenergySkaliertLeistungAufKilowatt(): void
{
self::assertSame(
[
'Ladeleistung' => 1.5,
'Entladeleistung' => 0.0,
'Modus' => 3,
'Management' => 1,
],
BatterieRegler::registerwerte(
BatterieRegler::TYP_SIGENERGY,
BatterieRegler::STEUERUNG_ENELIX,
1500
)
);
self::assertSame(
[
'Ladeleistung' => 0.0,
'Entladeleistung' => 1.5,
'Modus' => 6,
'Management' => 1,
],
BatterieRegler::registerwerte(
BatterieRegler::TYP_SIGENERGY,
BatterieRegler::STEUERUNG_ENELIX,
-1500
)
);
}
public function testUngueltigeGrenzenWerdenAbgewiesen(): void
{
$this->expectException(InvalidArgumentException::class);
BatterieRegler::leistungsraster(-1.0, 1000.0);
}
}
+11 -1
View File
@@ -12,7 +12,6 @@ final class DokumentationsstrukturTest extends TestCase
public function modulReadmes(): array
{
return [
['Batterie'],
['Waermepumpe'],
];
}
@@ -29,6 +28,17 @@ final class DokumentationsstrukturTest extends TestCase
self::assertStringContainsString('Properties', $inhalt);
}
public function testBatterieDokumentiertDenImplementiertenStand(): void
{
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Batterie/README.md');
self::assertNotFalse($inhalt);
self::assertStringContainsString('Status: implementiert', $inhalt);
self::assertStringContainsString('Properties', $inhalt);
self::assertStringContainsString('Registeradapter', $inhalt);
self::assertFileExists(__DIR__ . '/../docs/Schnittstelle-Batterie.md');
self::assertFileExists(__DIR__ . '/../docs/migration/Batterie.md');
}
public function testPufferspeicherDokumentiertDenImplementiertenStand(): void
{
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Pufferspeicher/README.md');
+10
View File
@@ -3,6 +3,16 @@
declare(strict_types=1);
return [
'Batterie' => [
'file' => __DIR__ . '/modules/Batterie.php',
'paths' => [
'Batterie/*',
'libs/BatterieRegler.php',
'libs/VerbraucherBasisTrait.php',
'libs/VerbraucherSchnittstelle.php',
'libs/Nachrichtenvertrag.php',
],
],
'EaseeGateway' => [
'file' => __DIR__ . '/modules/EaseeGateway.php',
'paths' => [
+180
View File
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
use Belevo\EnelixEMS\SymconTest\TestContext;
return static function (TestContext $test): void {
$numerischesRegister = static function (
TestContext $test,
string $name,
int $typ = 2
): int {
$variableID = $test->createVariable(
$typ,
$name,
$typ === 1 ? 0 : 0.0
);
$skriptID = IPS_CreateScript(0);
IPS_SetParent($skriptID, $test->rootId());
IPS_SetName($skriptID, $name . ' Aktion');
IPS_SetHidden($skriptID, true);
IPS_SetScriptContent($skriptID, <<<'PHP'
<?php
SetValue((int) $_IPS['VARIABLE'], $_IPS['VALUE']);
PHP
);
IPS_SetVariableCustomAction($variableID, $skriptID);
return $variableID;
};
$maxLadenID = $test->createVariable(2, 'Max Laden', 2000.0);
$maxEntladenID = $test->createVariable(2, 'Max Entladen', 2000.0);
$ladezustandID = $test->createVariable(2, 'Ladezustand', 50.0);
$netzleistungID = $test->createVariable(2, 'Netzleistung', 0.0);
$istleistungID = $test->createVariable(2, 'Batterieleistung', 0.0);
$managementID = $numerischesRegister($test, 'Management', 1);
$modusID = $numerischesRegister($test, 'Modus', 1);
$leistungID = $numerischesRegister($test, 'Leistung', 2);
$batterieID = $test->createInstance(
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
'Batterie GoodWe',
[
'Batterietyp' => 2,
'Batteriemanagement' => 2,
'MaxLadeleistungVariableID' => $maxLadenID,
'MaxEntladeleistungVariableID' => $maxEntladenID,
'LadezustandVariableID' => $ladezustandID,
'NetzleistungVariableID' => $netzleistungID,
'IstleistungVariableID' => $istleistungID,
'ManagementRegisterVariableID' => $managementID,
'ModusRegisterVariableID' => $modusID,
'LeistungsRegisterVariableID' => $leistungID,
'ReserveLadezustand' => 20.0,
'MindestLadezustand' => 10.0,
'LadezustandHysterese' => 2.0,
'MesswertMaxAlter' => 120,
'Aenderungssperre' => 0,
'DiagnosevariablenAnzeigen' => true,
]
);
$managerID = $test->createInstance(
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
'Hilfsmanager Batterie',
[
'AutomatischeSuche' => false,
'VerbraucherZuordnung' => json_encode([
['InstanzID' => $batterieID, 'Aktiv' => true],
], JSON_THROW_ON_ERROR),
]
);
$managerdatenSenden = static function (
string $betriebsart,
?int $leistung
) use ($managerID, $batterieID): void {
IPS_RequestAction($batterieID, 'ManagerdatenEmpfangen', json_encode([
'Kopf' => [
'Version' => '4.0',
'AbsenderID' => $managerID,
'EmpfaengerID' => $batterieID,
'Zeitpunkt' => time(),
],
'Betriebsart' => $betriebsart,
'Sollleistung_W' => $leistung,
], JSON_THROW_ON_ERROR));
};
$test->runCase(
'Instanz und Diagnosevariablen werden angelegt',
static function (TestContext $test) use ($batterieID): void {
$test->assertInstanceStatus($batterieID);
foreach ([
'Aktiv',
'Ladestatus',
'Istleistung',
'Sollleistung',
'Ladezustand',
'LadezustandGueltig',
'HystereseAktiv',
'LeistungsangebotDiagnose',
'LetzterRegisterbefehl',
] as $ident) {
$test->objectByIdent($ident, $batterieID);
}
}
);
$test->runCase(
'GoodWe-Laden schreibt Leistung Modus und Management direkt',
static function (TestContext $test) use (
$batterieID,
$managerdatenSenden,
$managementID,
$modusID,
$leistungID
): void {
IPS_RequestAction($batterieID, 'Aktiv', true);
$managerdatenSenden('PV', null);
$managerdatenSenden('PV', 500);
$test->waitUntil(
static fn (): bool => (float) GetValue($leistungID) === 500.0
&& (int) GetValue($modusID) === 11
&& (int) GetValue($managementID) === 2
);
}
);
$test->runCase(
'GoodWe-Entladen verwendet negativen Managerwert und Modus 12',
static function (TestContext $test) use (
$managerdatenSenden,
$modusID,
$leistungID
): void {
$managerdatenSenden('PV', -500);
$test->waitUntil(
static fn (): bool => (float) GetValue($leistungID) === 500.0
&& (int) GetValue($modusID) === 12
);
}
);
$test->runCase(
'Istleistungsereignis aktualisiert den Ladestatus ohne Regelzyklus',
static function (TestContext $test) use (
$batterieID,
$istleistungID
): void {
SetValue($istleistungID, -250.0);
$ladestatusID = $test->objectByIdent('Ladestatus', $batterieID);
$test->waitUntil(
static fn (): bool => (int) GetValue($ladestatusID) === 3
);
SetValue($istleistungID, 250.0);
$test->waitUntil(
static fn (): bool => (int) GetValue($ladestatusID) === 2
);
}
);
$test->runCase(
'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus',
static function (TestContext $test) use (
$batterieID,
$managementID,
$leistungID
): void {
$test->configureInstance($batterieID, ['Batteriemanagement' => 1]);
$test->waitUntil(
static fn (): bool => (float) GetValue($leistungID) === 0.0
&& (int) GetValue($managementID) === 1
);
}
);
};
+2
View File
@@ -59,6 +59,7 @@ final class SymconTestContractTest extends TestCase
{
self::assertSame(
[
'Batterie',
'LadestationGateway',
'LadestationStandAlone',
'Manager',
@@ -82,6 +83,7 @@ final class SymconTestContractTest extends TestCase
{
self::assertSame(
[
'Batterie',
'EaseeGateway',
'LadestationGateway',
'LadestationStandAlone',