diff --git a/examples/Demoanlage/README.md b/examples/Demoanlage/README.md new file mode 100644 index 0000000..7af8a1b --- /dev/null +++ b/examples/Demoanlage/README.md @@ -0,0 +1,53 @@ +# Enelix-2-Demoanlage + +Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symcon 8.x: + +- Enelix Manager mit manueller Zuordnung, +- zwei mehrstufige Boiler (1,2/2,4 kW und 1,5/3,0 kW), +- einen einstufigen Entfeuchter (0,9 kW), +- simulierte PV-Erzeugung, Grundlast, Netzleistung und Boilertemperaturen, +- Kachelvisualisierung mit Energiefluss und bedienbaren Simulationswerten, +- Archivierung der wichtigsten Leistungs- und Temperaturwerte. + +## Installation + +Das Repository muss in der IP-Symcon-Modulverwaltung auf dem gewünschten Stand installiert sein. +Danach in der Schnellausführung starten: + +```php +require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/examples/Demoanlage/bootstrap.php'; + +$result = enelixDemoInstall(); +print_r($result); +``` + +Ein weiterer Lauf aktualisiert dieselbe Anlage anhand stabiler Idents und erzeugt keine Duplikate. +Bestehende, nicht zur Demo gehörende Objekte werden nicht verändert oder gelöscht. + +## Bedienung + +In der Kachelvisualisierung öffnet die Startkategorie `Enelix 2 Demoanlage`. +Unter `Simulation` lassen sich Tageslauf, Tageszeit, Geschwindigkeit, +PV-Spitzenleistung, Bewölkung und Haus-Grundlast verändern. Der Manager verteilt +den verfügbaren PV-Überschuss nach Priorität auf beide Boiler und den Entfeuchter. + +## Validierung + +- Alle vier Enelix-Instanzen müssen Status 102 besitzen. +- Der Manager muss drei Verbraucher melden. +- Bei ausreichendem PV-Überschuss müssen Verbraucher schalten. +- Netzleistung, PV-Leistung, Verbraucherleistung und Temperaturen werden archiviert. +- Ein erneuter Installationslauf muss dieselbe Root-ID zurückgeben. + +## Rollback + +Nur die eindeutig gekennzeichnete Demo wird entfernt: + +```php +require_once IPS_GetKernelDir() . 'modules/Enelix-EMS/examples/Demoanlage/bootstrap.php'; + +enelixDemoRemove(true); +``` + +Der boolesche Parameter ist eine bewusste Löschbestätigung. Fremde Objektbäume +und Systeminstanzen bleiben unberührt. \ No newline at end of file diff --git a/examples/Demoanlage/bootstrap.php b/examples/Demoanlage/bootstrap.php new file mode 100644 index 0000000..3e9703d --- /dev/null +++ b/examples/Demoanlage/bootstrap.php @@ -0,0 +1,555 @@ + $temp1, + 'LeistungsStufen' => json_encode([ + ['Stufe' => 1, 'Leistung' => 1200, 'Schaltkontakt_Stufe' => $boiler11], + ['Stufe' => 2, 'Leistung' => 2400, 'Schaltkontakt_Stufe' => $boiler12], + ], JSON_THROW_ON_ERROR), + 'Boilervolumen' => 300, + 'LastwechselSperrzeit' => 2, + 'TemperaturMaxAlter' => 30, + 'LegionellenfunktionAktiv' => false, + 'PrioritaetPV' => 0, + 'PrioritaetPeak' => 2, + 'Meldeintervall' => 5, + 'VorgabeTimeout' => 30, + 'EinstellungenInVisu' => true, + 'DiagnosevariablenAnzeigen' => true, + ]); + enelixDemoConfigure($boiler2, [ + 'Boilerfuehler_PT1' => $temp2, + 'LeistungsStufen' => json_encode([ + ['Stufe' => 1, 'Leistung' => 1500, 'Schaltkontakt_Stufe' => $boiler21], + ['Stufe' => 2, 'Leistung' => 3000, 'Schaltkontakt_Stufe' => $boiler22], + ], JSON_THROW_ON_ERROR), + 'Boilervolumen' => 500, + 'LastwechselSperrzeit' => 2, + 'TemperaturMaxAlter' => 30, + 'LegionellenfunktionAktiv' => false, + 'PrioritaetPV' => 1, + 'PrioritaetPeak' => 1, + 'Meldeintervall' => 5, + 'VorgabeTimeout' => 30, + 'EinstellungenInVisu' => true, + 'DiagnosevariablenAnzeigen' => true, + ]); + enelixDemoConfigure($dryer, [ + 'Nennleistung' => 900, + 'SchaltkontaktVariableID' => $dryContact, + 'RueckmeldungVariableID' => $dryContact, + 'Mindesteinschaltdauer' => 2, + 'Mindestausschaltdauer' => 2, + 'Mindestlaufzeit' => 0, + 'PrioritaetPV' => 2, + 'PrioritaetPeak' => 0, + 'Meldeintervall' => 5, + 'VorgabeTimeout' => 30, + 'EinstellungenInVisu' => true, + 'DiagnosevariablenAnzeigen' => true, + ]); + + foreach ([$boiler1, $boiler2] as $boilerId) { + IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0); + IPS_RequestAction($boilerId, 'Maximaltemperatur', 62.0); + IPS_RequestAction($boilerId, 'Aktiv', true); + } + IPS_RequestAction($dryer, 'Aktiv', true); + + $manager = enelixDemoEnsureInstance($ems, 'Manager', 'Enelix Manager', ENELIX_DEMO_MANAGER_MODULE, 10); + $assignment = array_map( + static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true], + [$boiler1, $boiler2, $dryer] + ); + enelixDemoConfigure($manager, [ + 'NetzleistungVariableID' => $grid, + 'Netzleistungsfaktor' => 1.0, + 'MesswertMaxAlter' => 10, + 'AutomatischeSuche' => false, + 'SuchbereichID' => $root, + 'VerbraucherZuordnung' => json_encode($assignment, JSON_THROW_ON_ERROR), + 'KeepAlive' => 5, + 'VerbraucherTimeout' => 15, + 'SollwertSolarladen' => 0.0, + 'Umschaltdifferenz' => 0.0, + 'DiagnosevariablenAnzeigen' => true, + 'LoggingEin' => false, + ]); + + foreach ([$boiler1, $boiler2, $dryer] as $consumerId) { + IPS_RequestAction($consumerId, 'Melden', true); + } + IPS_RequestAction($manager, 'Aktiv', true); + + enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 50); + enelixDemoEnsureLink($dashboard, 'Boiler1Link', 'Boiler 1', $boiler1, 60); + enelixDemoEnsureLink($dashboard, 'Boiler2Link', 'Boiler 2', $boiler2, 70); + enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 80); + enelixDemoEnsureLink($simulation, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 70); + enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80); + + enelixDemoConfigureArchive([$pvDisplay, $loadDisplay, $grid, $temp1, $temp2]); + enelixDemoConfigureVisualization($root, $flow); + enelixDemoTick('Install'); + + return [ + 'rootId' => $root, + 'visualizationId' => enelixDemoFirstInstance(ENELIX_DEMO_TILE_MODULE), + 'managerId' => $manager, + 'consumerIds' => [$boiler1, $boiler2, $dryer], + 'tickScriptId' => $tickScript, + ]; +} + +function enelixDemoTick(string $source = 'Manual'): void +{ + $root = enelixDemoRequiredId(0, ENELIX_DEMO_ROOT_IDENT); + $dashboard = enelixDemoRequiredId($root, 'Dashboard'); + $simulation = enelixDemoRequiredId($root, 'Simulation'); + $ems = enelixDemoRequiredId($root, 'EMS'); + $technical = enelixDemoRequiredId($root, 'Technik'); + + $autoId = enelixDemoRequiredId($simulation, 'Automatik'); + $hourId = enelixDemoRequiredId($simulation, 'Tageszeit'); + $minutes = max(1, (int) GetValue(enelixDemoRequiredId($simulation, 'Geschwindigkeit'))); + $hour = (float) GetValue($hourId); + if ($source === 'Timer' && (bool) GetValue($autoId)) { + $hour = fmod($hour + ($minutes / 60.0), 24.0); + SetValue($hourId, $hour); + } + + $peak = (float) GetValue(enelixDemoRequiredId($simulation, 'PVSpitze')); + $cloud = (int) GetValue(enelixDemoRequiredId($simulation, 'Bewoelkung')); + $base = (float) GetValue(enelixDemoRequiredId($simulation, 'Grundlast')); + $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 - (min(100, max(0, $cloud)) * 0.0075)); + + $contacts = [ + 'boiler11' => enelixDemoRequiredId($technical, 'Boiler1Stufe1'), + 'boiler12' => enelixDemoRequiredId($technical, 'Boiler1Stufe2'), + 'boiler21' => enelixDemoRequiredId($technical, 'Boiler2Stufe1'), + 'boiler22' => enelixDemoRequiredId($technical, 'Boiler2Stufe2'), + 'dryer' => enelixDemoRequiredId($technical, 'EntfeuchterKontakt'), + ]; + $powerBefore = enelixDemoControlledPower($contacts); + $temp1 = enelixDemoRequiredId($technical, 'Boiler1Temperatur'); + $temp2 = enelixDemoRequiredId($technical, 'Boiler2Temperatur'); + if ($source === 'Timer') { + enelixDemoUpdateTemperature($temp1, (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0), $minutes); + enelixDemoUpdateTemperature($temp2, (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0), $minutes); + } + + $gridId = enelixDemoRequiredId($dashboard, 'Netzleistung'); + SetValue($gridId, round($base + $powerBefore - $pv, 1)); + + $consumers = [ + enelixDemoRequiredId($ems, 'Boiler1'), + enelixDemoRequiredId($ems, 'Boiler2'), + enelixDemoRequiredId($ems, 'Entfeuchter'), + ]; + foreach ($consumers as $consumerId) { + IPS_RequestAction($consumerId, 'Melden', true); + } + $manager = enelixDemoRequiredId($ems, 'Manager'); + IPS_RequestAction($manager, 'Regeln', true); + + $power = enelixDemoControlledPower($contacts); + $grid = $base + $power - $pv; + SetValue($gridId, round($grid, 1)); + SetValue(enelixDemoRequiredId($dashboard, 'PVLeistung'), round($pv, 1)); + SetValue(enelixDemoRequiredId($dashboard, 'Verbraucherleistung'), (float) $power); + + $state = [ + 'hour' => $hour, + 'pv' => $pv, + 'base' => $base, + 'grid' => $grid, + 'boiler1' => (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0), + 'boiler2' => (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0), + 'dryer' => (bool) GetValue($contacts['dryer']) ? 900 : 0, + 'temp1' => (float) GetValue($temp1), + 'temp2' => (float) GetValue($temp2), + 'manager' => (string) GetValue(enelixDemoRequiredId($manager, 'Betriebsart')), + ]; + SetValue(enelixDemoRequiredId($dashboard, 'Energiefluss'), enelixDemoRenderFlow($state)); +} + +function enelixDemoRenderFlow(array $state): string +{ + $format = static fn (float $value): string => number_format($value, 0, ',', "'"); + $hour = (float) $state['hour']; + $time = sprintf('%02d:%02d', (int) floor($hour), (int) round(($hour - floor($hour)) * 60) % 60); + $grid = (float) $state['grid']; + $gridLabel = $grid < -25 ? 'Einspeisung' : ($grid > 25 ? 'Netzbezug' : 'Ausgeglichen'); + $gridArrow = $grid < -25 ? '→' : ($grid > 25 ? '←' : '↔'); + $gridClass = abs($grid) <= 250 ? 'good' : ($grid < 0 ? 'export' : 'import'); + $b1Class = (float) $state['boiler1'] > 0 ? 'on' : ''; + $b2Class = (float) $state['boiler2'] > 0 ? 'on' : ''; + $dryClass = (float) $state['dryer'] > 0 ? 'on' : ''; + + return '
Enelix 2 Energiefluss
Manager regelt drei flexible Verbraucher
Simulation ' . $time . ' Uhr
+
PV-Erzeugung' . $format((float) $state['pv']) . ' W
→
Haus + flexible Lasten' . $format((float) $state['base'] + (float) $state['boiler1'] + (float) $state['boiler2'] + (float) $state['dryer']) . ' W
' . $gridArrow . '
' . $gridLabel . '' . $format(abs($grid)) . ' W
+
Priorität 1Boiler 1 · ' . $format((float) $state['boiler1']) . ' W' . number_format((float) $state['temp1'], 1, ',', '') . ' °C
Priorität 2Boiler 2 · ' . $format((float) $state['boiler2']) . ' W' . number_format((float) $state['temp2'], 1, ',', '') . ' °C
Priorität 3Entfeuchter · ' . $format((float) $state['dryer']) . ' Weinstufiger Verbraucher
+
Grundlast ' . $format((float) $state['base']) . ' WManager: ' . htmlspecialchars((string) $state['manager'], ENT_QUOTES, 'UTF-8') . '
'; +} + +function enelixDemoControlledPower(array $contacts): int +{ + $boiler1 = (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0); + $boiler2 = (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0); + return $boiler1 + $boiler2 + ((bool) GetValue($contacts['dryer']) ? 900 : 0); +} + +function enelixDemoUpdateTemperature(int $variableId, int $power, int $minutes): void +{ + $current = (float) GetValue($variableId); + $delta = (($power / 1000.0) * 0.03 * $minutes) - (0.01 * $minutes); + SetValue($variableId, round(min(70.0, max(38.0, $current + $delta)), 2)); +} + +function enelixDemoValidateRuntime(): void +{ + $version = IPS_GetKernelVersion(); + if (version_compare($version, '8.0', '<') || version_compare($version, '9.0', '>=')) { + throw new RuntimeException('Die Demo benötigt IP-Symcon 8.x; aktiv ist ' . $version . '.'); + } + $available = IPS_GetModuleList(); + foreach ([ENELIX_DEMO_MANAGER_MODULE, ENELIX_DEMO_BOILER_MODULE, ENELIX_DEMO_SINGLE_MODULE] as $moduleId) { + if (!in_array($moduleId, $available, true)) { + throw new RuntimeException('Benötigtes Enelix-Modul fehlt: ' . $moduleId); + } + } +} + +function enelixDemoEnsureProfiles(): void +{ + enelixDemoEnsureProfile('ENELIX.Demo.Power', 2, -12000, 12000, 100, 0, ' W'); + enelixDemoEnsureProfile('ENELIX.Demo.PowerPositive', 2, 0, 12000, 100, 0, ' W'); + enelixDemoEnsureProfile('ENELIX.Demo.Hour', 2, 0, 23.75, 0.25, 2, ' h'); + enelixDemoEnsureProfile('ENELIX.Demo.Percent', 1, 0, 100, 5, 0, ' %'); + enelixDemoEnsureProfile('ENELIX.Demo.Speed', 1, 1, 60, 1, 0, ' min'); +} + +function enelixDemoEnsureProfile(string $name, int $type, float $min, float $max, float $step, int $digits, string $suffix): void +{ + if (!IPS_VariableProfileExists($name)) { + IPS_CreateVariableProfile($name, $type); + } + $profile = IPS_GetVariableProfile($name); + if ((int) $profile['ProfileType'] !== $type) { + throw new RuntimeException('Variablenprofil hat einen unpassenden Typ: ' . $name); + } + IPS_SetVariableProfileValues($name, $min, $max, $step); + IPS_SetVariableProfileDigits($name, $digits); + IPS_SetVariableProfileText($name, '', $suffix); +} + +function enelixDemoEnsureCategory(int $parent, string $ident, string $name, int $position, bool $hidden = false): int +{ + $id = enelixDemoFind($parent, $ident); + if ($id === 0) { + $id = IPS_CreateCategory(); + IPS_SetParent($id, $parent); + IPS_SetIdent($id, $ident); + } + enelixDemoRequireType($id, 0); + IPS_SetName($id, $name); + IPS_SetPosition($id, $position); + IPS_SetHidden($id, $hidden); + return $id; +} + +function enelixDemoEnsureVariable(int $parent, string $ident, string $name, int $type, string $profile, $default, int $position): int +{ + $id = enelixDemoFind($parent, $ident); + $created = false; + if ($id === 0) { + $id = IPS_CreateVariable($type); + IPS_SetParent($id, $parent); + IPS_SetIdent($id, $ident); + $created = true; + } + enelixDemoRequireType($id, 2); + $variable = IPS_GetVariable($id); + if ((int) $variable['VariableType'] !== $type) { + throw new RuntimeException('Variablentyp-Konflikt bei ' . $ident); + } + IPS_SetName($id, $name); + IPS_SetPosition($id, $position); + if ($profile !== '') { + IPS_SetVariableCustomProfile($id, $profile); + } + if ($created) { + SetValue($id, $default); + } + return $id; +} + +function enelixDemoEnsureScript(int $parent, string $ident, string $name, string $content, int $position): int +{ + $id = enelixDemoFind($parent, $ident); + if ($id === 0) { + $id = IPS_CreateScript(0); + IPS_SetParent($id, $parent); + IPS_SetIdent($id, $ident); + } + enelixDemoRequireType($id, 3); + IPS_SetName($id, $name); + IPS_SetPosition($id, $position); + IPS_SetScriptContent($id, $content); + return $id; +} + +function enelixDemoEnsureInstance(int $parent, string $ident, string $name, string $moduleId, int $position): int +{ + $id = enelixDemoFind($parent, $ident); + if ($id === 0) { + $id = IPS_CreateInstance($moduleId); + IPS_SetParent($id, $parent); + IPS_SetIdent($id, $ident); + } + enelixDemoRequireType($id, 1); + $actualModule = IPS_GetInstance($id)['ModuleInfo']['ModuleID']; + if ($actualModule !== $moduleId) { + throw new RuntimeException('Modulkonflikt bei Instanz ' . $ident); + } + IPS_SetName($id, $name); + IPS_SetPosition($id, $position); + return $id; +} + +function enelixDemoEnsureLink(int $parent, string $ident, string $name, int $target, int $position): int +{ + $id = enelixDemoFind($parent, $ident); + if ($id === 0) { + $id = IPS_CreateLink(); + IPS_SetParent($id, $parent); + IPS_SetIdent($id, $ident); + } + enelixDemoRequireType($id, 6); + IPS_SetName($id, $name); + IPS_SetPosition($id, $position); + IPS_SetLinkTargetID($id, $target); + return $id; +} + +function enelixDemoEnsureCyclicEvent(int $scriptId, string $ident, int $seconds): int +{ + $id = enelixDemoFind($scriptId, $ident); + if ($id === 0) { + $id = IPS_CreateEvent(1); + IPS_SetParent($id, $scriptId); + IPS_SetIdent($id, $ident); + } + enelixDemoRequireType($id, 4); + IPS_SetName($id, 'Alle ' . $seconds . ' Sekunden'); + IPS_SetEventCyclic($id, 0, 0, 0, 0, 1, $seconds); + IPS_SetEventActive($id, true); + return $id; +} + +function enelixDemoConfigure(int $instanceId, array $properties): void +{ + foreach ($properties as $name => $value) { + IPS_SetProperty($instanceId, (string) $name, $value); + } + IPS_ApplyChanges($instanceId); + if ((int) IPS_GetInstance($instanceId)['InstanceStatus'] !== 102) { + throw new RuntimeException('Instanz ist nach der Konfiguration nicht aktiv: ' . IPS_GetName($instanceId)); + } +} + +function enelixDemoConfigureArchive(array $variableIds): void +{ + $archive = enelixDemoFirstInstance(ENELIX_DEMO_ARCHIVE_MODULE); + if ($archive === 0) { + return; + } + foreach ($variableIds as $variableId) { + AC_SetLoggingStatus($archive, $variableId, true); + AC_SetAggregationType($archive, $variableId, 0); + } + IPS_ApplyChanges($archive); +} + +function enelixDemoConfigureVisualization(int $root, int $flow): void +{ + $visualization = enelixDemoFirstInstance(ENELIX_DEMO_TILE_MODULE); + if ($visualization === 0) { + return; + } + IPS_SetProperty($visualization, 'BaseID', $root); + IPS_SetProperty($visualization, 'CategoryDisplay', 'Cards'); + IPS_SetProperty($visualization, 'ShowGreeting', 3); + IPS_SetProperty($visualization, 'GreetingName', 'Enelix 2 Demo'); + IPS_SetProperty($visualization, 'ShowDateTime', true); + IPS_SetProperty($visualization, 'Favorites', json_encode([['ObjectID' => $flow]], JSON_THROW_ON_ERROR)); + IPS_ApplyChanges($visualization); +} + +function enelixDemoFirstInstance(string $moduleId): int +{ + $ids = IPS_GetInstanceListByModuleID($moduleId); + return $ids === [] ? 0 : (int) $ids[0]; +} + +function enelixDemoFind(int $parent, string $ident): int +{ + $id = @IPS_GetObjectIDByIdent($ident, $parent); + return $id === false ? 0 : (int) $id; +} + +function enelixDemoRequiredId(int $parent, string $ident): int +{ + $id = enelixDemoFind($parent, $ident); + if ($id === 0) { + throw new RuntimeException('Demo-Objekt fehlt: ' . $ident); + } + return $id; +} + +function enelixDemoRequireType(int $objectId, int $type): void +{ + $actual = (int) IPS_GetObject($objectId)['ObjectType']; + if ($actual !== $type) { + throw new RuntimeException('Objekttyp-Konflikt bei ID ' . $objectId); + } +} + +function enelixDemoRemove(bool $confirmed = false): void +{ + if (!$confirmed) { + throw new RuntimeException('Rollback nur mit enelixDemoRemove(true) ausführen.'); + } + $root = enelixDemoFind(0, ENELIX_DEMO_ROOT_IDENT); + if ($root === 0) { + return; + } + $visualization = enelixDemoFirstInstance(ENELIX_DEMO_TILE_MODULE); + if ($visualization > 0 && (int) json_decode(IPS_GetConfiguration($visualization), true)['BaseID'] === $root) { + IPS_SetProperty($visualization, 'BaseID', 0); + IPS_ApplyChanges($visualization); + } + enelixDemoDeleteTree($root); + foreach (['ENELIX.Demo.Power', 'ENELIX.Demo.PowerPositive', 'ENELIX.Demo.Hour', 'ENELIX.Demo.Percent', 'ENELIX.Demo.Speed'] as $profile) { + if (IPS_VariableProfileExists($profile)) { + IPS_DeleteVariableProfile($profile); + } + } +} + +function enelixDemoDeleteTree(int $objectId): void +{ + foreach (array_reverse(IPS_GetChildrenIDs($objectId)) as $childId) { + enelixDemoDeleteTree((int) $childId); + } + $type = (int) IPS_GetObject($objectId)['ObjectType']; + if ($type === 1 && (int) IPS_GetInstance($objectId)['ConnectionID'] > 0) { + IPS_DisconnectInstance($objectId); + } + switch ($type) { + case 0: IPS_DeleteCategory($objectId); break; + case 1: IPS_DeleteInstance($objectId); break; + case 2: IPS_DeleteVariable($objectId); break; + case 3: IPS_DeleteScript($objectId, true); break; + case 4: IPS_DeleteEvent($objectId); break; + case 5: IPS_DeleteMedia($objectId, true); break; + case 6: IPS_DeleteLink($objectId); break; + default: throw new RuntimeException('Unbekannter Objekttyp: ' . $type); + } +} diff --git a/tests/DemoanlageBootstrapTest.php b/tests/DemoanlageBootstrapTest.php new file mode 100644 index 0000000..8110f20 --- /dev/null +++ b/tests/DemoanlageBootstrapTest.php @@ -0,0 +1,41 @@ + 12.5, + 'pv' => 6500.0, + 'base' => 900.0, + 'grid' => -800.0, + 'boiler1' => 2400, + 'boiler2' => 1500, + 'dryer' => 900, + 'temp1' => 51.2, + 'temp2' => 48.7, + 'manager' => 'PV', + ]); + + self::assertStringContainsString('Enelix 2 Energiefluss', $html); + self::assertStringContainsString('Boiler 1', $html); + self::assertStringContainsString('Boiler 2', $html); + self::assertStringContainsString('Entfeuchter', $html); + self::assertStringContainsString('Einspeisung', $html); + self::assertStringContainsString('12:30', $html); + } + + public function testRollbackIstExplizitBestaetigungspflichtig(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Rollback nur mit'); + + enelixDemoRemove(); + } +}