feat(utils): adapt virtual battery and VGT interface
Tests / test (push) Successful in 46s

This commit is contained in:
dh
2026-09-27 12:28:32 +00:00
parent 92eec804a7
commit f11b1fbe61
22 changed files with 2445 additions and 104 deletions
+2 -2
View File
@@ -13,10 +13,10 @@ final class DokumentationsstrukturTest extends TestCase
{
return [
'Verbrauchskostenreport' => ['Verbrauchskostenreport', 'Status: Implementiert'],
'Virtuelle Batterie' => ['Virtuelle-Batterie', 'Status: Diskussionsentwurf'],
'Virtuelle Batterie' => ['Virtuelle-Batterie', 'Status: Implementiert'],
'CC100 Hardware' => ['CC100-Hardware', 'Status: Implementiert'],
'Energiediagramm' => ['Energiediagramm', 'Status: Implementiert'],
'VGT-Schnittstelle' => ['VGT-Schnittstelle', 'Status: Diskussionsentwurf'],
'VGT-Schnittstelle' => ['VGT-Schnittstelle', 'Status: Implementiert'],
'Shelly-Modul' => ['Shelly-Modul', 'Status: Implementiert'],
];
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixUtils\Tests;
use PHPUnit\Framework\TestCase;
final class NeueModuleStrukturTest extends TestCase
{
/** @dataProvider module */
public function testMetadatenUndFormulareSindGueltig(
string $ordner,
string $name,
string $prefix
): void {
$metadata = json_decode(
(string) file_get_contents(
__DIR__ . '/../' . $ordner . '/module.json'
),
true,
512,
JSON_THROW_ON_ERROR
);
self::assertSame($name, $metadata['name']);
self::assertSame($prefix, $metadata['prefix']);
self::assertSame(3, $metadata['type']);
self::assertMatchesRegularExpression(
'/^\{[0-9A-F-]{36}\}$/',
$metadata['id']
);
$formular = json_decode(
(string) file_get_contents(
__DIR__ . '/../' . $ordner . '/form.json'
),
true,
512,
JSON_THROW_ON_ERROR
);
self::assertNotEmpty($formular['elements']);
}
/** @return array<string, array{string, string, string}> */
public function module(): array
{
return [
'Virtuelle Batterie' => [
'VirtuelleBatterie',
'Virtuelle Batterie',
'ENLVB',
],
'VGT-Schnittstelle' => [
'VGTSchnittstelle',
'VGTSchnittstelle',
'ENLVGT',
],
];
}
public function testVirtuelleBatterieStelltEVProxyregisterBereit(): void
{
$modul = (string) file_get_contents(
__DIR__ . '/../VirtuelleBatterie/module.php'
);
foreach ([
"'EVManagement'",
"'EVModus'",
"'EVLadeleistung'",
"'EVEntladeleistung'",
"'SDLSollleistung'",
'VirtuelleBatterieRegler::kombiniere',
] as $bestandteil) {
self::assertStringContainsString($bestandteil, $modul);
}
}
public function testPhysischeSollwerteWerdenNurPerActionGeschrieben(): void
{
$modul = (string) file_get_contents(
__DIR__ . '/../VirtuelleBatterie/module.php'
);
$anfang = strpos($modul, 'private function schreibeVerteilung');
$ende = strpos($modul, 'private function sichererZustand');
self::assertNotFalse($anfang);
self::assertNotFalse($ende);
$funktion = substr($modul, $anfang, $ende - $anfang);
self::assertStringContainsString('RequestAction(', $funktion);
self::assertStringNotContainsString('SetValue($zielID', $funktion);
}
}
+12
View File
@@ -15,6 +15,18 @@ return [
'Energiediagramm/*',
],
],
'VGTSchnittstelle' => [
'file' => __DIR__ . '/modules/VGTSchnittstelle.php',
'paths' => [
'VGTSchnittstelle/*',
],
],
'VirtuelleBatterie' => [
'file' => __DIR__ . '/modules/VirtuelleBatterie.php',
'paths' => [
'VirtuelleBatterie/*',
],
],
'ShellyModul' => [
'file' => __DIR__ . '/modules/ShellyModul.php',
'paths' => [
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
use Belevo\EnelixUtils\SymconTest\TestContext;
return static function (TestContext $test): void {
$instancesBefore = array_flip(IPS_GetInstanceList());
$mqttId = $test->createInstance(
'{C6D2AEB3-6E1F-4B2E-8E69-3A1A00246850}',
'MQTT-Testserver'
);
$ioId = (int) IPS_GetInstance($mqttId)['ConnectionID'];
if ($ioId > 0 && !isset($instancesBefore[$ioId])) {
IPS_SetParent($ioId, $test->rootId());
}
$moduleId = $test->createInstance(
'{4BA1A557-7970-4FB9-93B8-1F6ADA0A5A94}',
'VGT-Schnittstelle'
);
if ((int) IPS_GetInstance($moduleId)['ConnectionID'] !== $mqttId) {
IPS_DisconnectInstance($moduleId);
IPS_ConnectInstance($moduleId, $mqttId);
IPS_ApplyChanges($moduleId);
}
$test->runCase('MQTT-Elterninstanz wird verwendet', static function (
TestContext $test
) use ($moduleId, $mqttId): void {
$test->assertSame(
$mqttId,
(int) IPS_GetInstance($moduleId)['ConnectionID']
);
});
$test->runCase('Konfigurationsformular ist gueltiges JSON', static function (
TestContext $test
) use ($moduleId): void {
$form = json_decode(
IPS_GetConfigurationForm($moduleId),
true,
512,
JSON_THROW_ON_ERROR
);
$test->assertTrue(is_array($form));
});
};
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use Belevo\EnelixUtils\SymconTest\TestContext;
return static function (TestContext $test): void {
$moduleId = $test->createInstance(
'{5B2A199B-2226-411F-B83B-FC1CBA58496B}',
'Virtuelle Batterie'
);
$test->runCase('Konfigurationsformular ist gueltiges JSON', static function (
TestContext $test
) use ($moduleId): void {
$form = json_decode(
IPS_GetConfigurationForm($moduleId),
true,
512,
JSON_THROW_ON_ERROR
);
$test->assertTrue(is_array($form));
});
$test->runCase('Sichere Startkonfiguration ist inaktiv', static function (
TestContext $test
) use ($moduleId): void {
$aktivId = IPS_GetObjectIDByIdent('Aktiv', $moduleId);
$test->assertSame(false, GetValue($aktivId));
});
};
+8 -1
View File
@@ -58,7 +58,14 @@ final class SymconTestContractTest extends TestCase
public function testAffectedModuleResolverSelectsAllModulesForFrameworkChanges(): void
{
self::assertSame(
['CC100Hardware', 'Energiediagramm', 'ShellyModul', 'Verbrauchskostenreport'],
[
'CC100Hardware',
'Energiediagramm',
'ShellyModul',
'VGTSchnittstelle',
'Verbrauchskostenreport',
'VirtuelleBatterie',
],
$this->resolveAffectedModules(['tests/Symcon/TestContext.php'])
);
}
+77
View File
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixUtils\Tests;
use Belevo\EnelixUtils\VGTRegler;
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../VGTSchnittstelle/libs/VGTRegler.php';
final class VGTReglerTest extends TestCase
{
public function testPVActivateUndStopBleibenKompatibel(): void
{
self::assertSame(
4000.0,
VGTRegler::zielwert(0, 'activate', 4000.0, 10000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0)
);
self::assertSame(
10000.0,
VGTRegler::zielwert(0, 'stop', 4000.0, 10000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0)
);
}
public function testBatterieActivateInvertiertDenSetpointWieEnelixEins(): void
{
self::assertSame(
-3500.0,
VGTRegler::zielwert(1, 'activate', 3500.0, 10000.0, 50.0, 50.0, 2500.0, 2500.0, 10.0, 90.0)
);
}
public function testRestoreRegeltAufZielLadezustand(): void
{
self::assertSame(
2500.0,
VGTRegler::zielwert(1, 'restore', 0.0, 10000.0, 40.0, 50.0, 2500.0, 3000.0, 10.0, 90.0)
);
self::assertSame(
-3000.0,
VGTRegler::zielwert(1, 'restore', 0.0, 10000.0, 60.0, 50.0, 2500.0, 3000.0, 10.0, 90.0)
);
}
public function testLadezustandsgrenzenStoppenFalscheRichtung(): void
{
self::assertSame(
0.0,
VGTRegler::zielwert(1, 'activate', 3000.0, 10000.0, 10.0, 50.0, 2500.0, 2500.0, 10.0, 90.0)
);
}
public function testFesteMQTTSchnittstelleBleibtImModulErhalten(): void
{
$modul = file_get_contents(
__DIR__ . '/../VGTSchnittstelle/module.php'
);
self::assertNotFalse($modul);
foreach ([
'feedback-request/',
'feedback-response/',
'remote-control-request/',
'remote-control-response/',
"'power_setpoint'",
"'strategy'",
"'power_production'",
"'is_ready'",
"'is_running'",
"'state_of_charge'",
"'min_soc'",
"'max_soc'",
] as $bestandteil) {
self::assertStringContainsString($bestandteil, $modul);
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixUtils\Tests;
use Belevo\EnelixUtils\VirtuelleBatterieRegler;
use PHPUnit\Framework\TestCase;
require_once __DIR__
. '/../VirtuelleBatterie/libs/VirtuelleBatterieRegler.php';
final class VirtuelleBatterieReglerTest extends TestCase
{
public function testAggregatIstKapazitaetsgewichtet(): void
{
$aggregat = VirtuelleBatterieRegler::aggregiere([
$this->batterie(0, 10.0, 20.0, 5000.0, 5000.0),
$this->batterie(1, 30.0, 80.0, 10000.0, 8000.0),
], 3000.0, 2000.0, 0.5);
self::assertSame(40.0, $aggregat['Kapazitaet_kWh']);
self::assertSame(65.0, $aggregat['Ladezustand_Prozent']);
self::assertSame(15000.0, $aggregat['MaxLaden_W']);
self::assertSame(13000.0, $aggregat['MaxEntladen_W']);
self::assertSame(2.5, $aggregat['SDLKapazitaet_kWh']);
self::assertSame(37.5, $aggregat['EigenverbrauchKapazitaet_kWh']);
}
public function testSDLHatBeiGleicherRichtungVorrang(): void
{
$aggregat = VirtuelleBatterieRegler::aggregiere([
$this->batterie(0, 20.0, 50.0, 10000.0, 10000.0),
], 6000.0, 6000.0, 0.5);
$auftrag = VirtuelleBatterieRegler::kombiniere(
8000.0,
6000.0,
$aggregat
);
self::assertSame(6000.0, $auftrag['SDL_W']);
self::assertSame(4000.0, $auftrag['Eigenverbrauch_W']);
self::assertSame(10000.0, $auftrag['Gesamt_W']);
}
public function testGegenlaeufigeAuftraegeWerdenNettoVerteilt(): void
{
$aggregat = VirtuelleBatterieRegler::aggregiere([
$this->batterie(0, 20.0, 50.0, 10000.0, 10000.0),
], 6000.0, 6000.0, 0.5);
$auftrag = VirtuelleBatterieRegler::kombiniere(
4000.0,
-3000.0,
$aggregat
);
self::assertSame(1000.0, $auftrag['Gesamt_W']);
self::assertSame(-3000.0, $auftrag['SDL_W']);
self::assertSame(4000.0, $auftrag['Eigenverbrauch_W']);
}
public function testLadenBeginntBeiNiedrigstemLadezustand(): void
{
$verteilung = VirtuelleBatterieRegler::verteile(7000.0, [
$this->batterie(0, 10.0, 80.0, 5000.0, 5000.0),
$this->batterie(1, 10.0, 20.0, 5000.0, 5000.0),
]);
self::assertSame(2000.0, $verteilung[0]);
self::assertSame(5000.0, $verteilung[1]);
}
public function testIstleistungOrdnetSDLZuerstZu(): void
{
self::assertSame(
['Eigenverbrauch_W' => 4000.0, 'SDL_W' => -3000.0],
VirtuelleBatterieRegler::ordneIstleistungZu(1000.0, -3000.0)
);
}
/**
* @return array{
* Index:int,
* Name:string,
* Kapazitaet_kWh:float,
* MaxLaden_W:float,
* MaxEntladen_W:float,
* Ladezustand_Prozent:float,
* Istleistung_W:float
* }
*/
private function batterie(
int $index,
float $kapazitaet,
float $ladezustand,
float $maxLaden,
float $maxEntladen
): array {
return [
'Index' => $index,
'Name' => 'Batterie ' . $index,
'Kapazitaet_kWh' => $kapazitaet,
'MaxLaden_W' => $maxLaden,
'MaxEntladen_W' => $maxEntladen,
'Ladezustand_Prozent' => $ladezustand,
'Istleistung_W' => 0.0,
];
}
}