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
+
';
+}
+
+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();
+ }
+}