From abd8c7edeb1a9779a2b24e3db2a98743b8745d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Thu, 17 Sep 2026 14:06:12 +0000 Subject: [PATCH] Enhance demo visualization and charts --- examples/Demoanlage/README.md | 17 +- examples/Demoanlage/bootstrap.php | 301 +++++++++++++++++++++++++++--- tests/DemoanlageBootstrapTest.php | 41 ++++ 3 files changed, 326 insertions(+), 33 deletions(-) diff --git a/examples/Demoanlage/README.md b/examples/Demoanlage/README.md index 7af8a1b..299bee8 100644 --- a/examples/Demoanlage/README.md +++ b/examples/Demoanlage/README.md @@ -6,8 +6,11 @@ Die Demo erzeugt idempotent eine vollständige, spielbare EMS-Anlage in IP-Symco - 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. +- native Symcon-Instanz `Energy Distribution` für den Energiefluss, +- eigene Kachelansichten mit allen Laufzeitparametern je Boiler, +- kombinierten Verlauf für PV-Leistung, Hausverbrauch und Netzleistung, +- kombinierten Verlauf der kumulierten PV-, Haus-, Bezugs- und Einspeiseenergie, +- Archivierung der Leistungs-, Energie- und Temperaturwerte. ## Installation @@ -31,12 +34,20 @@ 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. +`Übersicht` zeigt die native Symcon-Energieverteilung. Die Kacheln `Boiler 1` +und `Boiler 2` öffnen direkt sämtliche Bedien-, Status- und Diagnosevariablen, +ohne zuerst das Instanz-Konfigurationsformular zu zeigen. Unter `Leistung und +Energie` liegen die beiden kombinierten Zeitdiagramme und die Energiezähler. +Die Energiesummen verwenden die beschleunigte Simulationszeit. + ## 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. +- Netzleistung, PV-Leistung, Hausverbrauch, Verbraucherleistungen, Energiesummen und Temperaturen werden archiviert. +- Die native Energieverteilung muss sechs Knoten enthalten. +- Beide Diagrammkacheln müssen mindestens drei beschriftete Zeitreihen anzeigen. - Ein erneuter Installationslauf muss dieselbe Root-ID zurückgeben. ## Rollback diff --git a/examples/Demoanlage/bootstrap.php b/examples/Demoanlage/bootstrap.php index 3e9703d..5c13909 100644 --- a/examples/Demoanlage/bootstrap.php +++ b/examples/Demoanlage/bootstrap.php @@ -8,6 +8,7 @@ const ENELIX_DEMO_BOILER_MODULE = '{B7C54AF4-AD7D-4FE4-B75D-203693906251}'; const ENELIX_DEMO_SINGLE_MODULE = '{15879A4E-D0C2-4495-83DE-46E1E462591E}'; const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}'; const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}'; +const ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}'; function enelixDemoInstall(): array { @@ -16,17 +17,32 @@ function enelixDemoInstall(): array $root = enelixDemoEnsureCategory(0, ENELIX_DEMO_ROOT_IDENT, 'Enelix 2 Demoanlage', 100); IPS_SetInfo($root, 'Idempotent erzeugte Enelix-2-Demo. Entfernen nur mit enelixDemoRemove(true).'); - $dashboard = enelixDemoEnsureCategory($root, 'Dashboard', 'Energiefluss', 10); + $dashboard = enelixDemoEnsureCategory($root, 'Dashboard', 'Übersicht', 10); $simulation = enelixDemoEnsureCategory($root, 'Simulation', 'Simulation', 20); - $ems = enelixDemoEnsureCategory($root, 'EMS', 'Manager und Verbraucher', 30); + $boiler1View = enelixDemoEnsureCategory($root, 'Boiler1Ansicht', 'Boiler 1', 30); + $boiler2View = enelixDemoEnsureCategory($root, 'Boiler2Ansicht', 'Boiler 2', 40); + $history = enelixDemoEnsureCategory($root, 'Historie', 'Leistung und Energie', 50); + $ems = enelixDemoEnsureCategory($root, 'EMS', 'Manager und Verbraucher', 60); $technical = enelixDemoEnsureCategory($root, 'Technik', 'Simulationstechnik', 90, true); enelixDemoEnsureProfiles(); - $flow = enelixDemoEnsureVariable($dashboard, 'Energiefluss', 'Energiefluss', 3, '~HTMLBox', '', 10); + $legacyFlow = enelixDemoEnsureVariable($dashboard, 'Energiefluss', 'Energiefluss (Legacy)', 3, '~HTMLBox', '', 90); + IPS_SetHidden($legacyFlow, true); $pvDisplay = enelixDemoEnsureVariable($dashboard, 'PVLeistung', 'PV-Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 20); - $loadDisplay = enelixDemoEnsureVariable($dashboard, 'Verbraucherleistung', 'Geregelte Verbraucher', 2, 'ENELIX.Demo.PowerPositive', 0.0, 30); - $grid = enelixDemoEnsureVariable($dashboard, 'Netzleistung', 'Netzleistung', 2, 'ENELIX.Demo.Power', 0.0, 40); + $houseDisplay = enelixDemoEnsureVariable($dashboard, 'Hausverbrauch', 'Hausverbrauch gesamt', 2, 'ENELIX.Demo.PowerPositive', 0.0, 30); + $loadDisplay = enelixDemoEnsureVariable($dashboard, 'Verbraucherleistung', 'Geregelte Verbraucher', 2, 'ENELIX.Demo.PowerPositive', 0.0, 40); + $grid = enelixDemoEnsureVariable($dashboard, 'Netzleistung', 'Netzleistung', 2, 'ENELIX.Demo.Power', 0.0, 50); + $boiler1Power = enelixDemoEnsureVariable($dashboard, 'Boiler1Leistung', 'Boiler 1 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 60); + $boiler2Power = enelixDemoEnsureVariable($dashboard, 'Boiler2Leistung', 'Boiler 2 Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 70); + $dryerPower = enelixDemoEnsureVariable($dashboard, 'EntfeuchterLeistung', 'Entfeuchter Leistung', 2, 'ENELIX.Demo.PowerPositive', 0.0, 80); + + $powerChart = enelixDemoEnsureVariable($history, 'Leistungsdiagramm', 'Leistungsverlauf', 3, '~HTMLBox', '', 10); + $energyChart = enelixDemoEnsureVariable($history, 'Energiediagramm', 'Kumulierte Energien', 3, '~HTMLBox', '', 20); + $pvEnergy = enelixDemoEnsureVariable($history, 'PVEnergie', 'PV-Energie', 2, 'ENELIX.Demo.Energy', 0.0, 30); + $houseEnergy = enelixDemoEnsureVariable($history, 'HausEnergie', 'Hausverbrauch Energie', 2, 'ENELIX.Demo.Energy', 0.0, 40); + $gridImportEnergy = enelixDemoEnsureVariable($history, 'NetzbezugEnergie', 'Netzbezug Energie', 2, 'ENELIX.Demo.Energy', 0.0, 50); + $gridExportEnergy = enelixDemoEnsureVariable($history, 'EinspeisungEnergie', 'Einspeisung Energie', 2, 'ENELIX.Demo.Energy', 0.0, 60); $auto = enelixDemoEnsureVariable($simulation, 'Automatik', 'Tageslauf automatisch', 0, '~Switch', true, 10); $hour = enelixDemoEnsureVariable($simulation, 'Tageszeit', 'Simulierte Tageszeit', 2, 'ENELIX.Demo.Hour', 8.0, 20); @@ -70,6 +86,8 @@ function enelixDemoInstall(): array $boiler21 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe1', 'Boiler 2 Stufe 1', 0, '~Switch', false, 140); $boiler22 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe2', 'Boiler 2 Stufe 2', 0, '~Switch', false, 150); $dryContact = enelixDemoEnsureVariable($technical, 'EntfeuchterKontakt', 'Entfeuchter Kontakt', 0, '~Switch', false, 160); + $chartUpdated = enelixDemoEnsureVariable($technical, 'LetztesDiagrammUpdate', 'Letztes Diagramm-Update', 1, '', 0, 170); + IPS_SetHidden($chartUpdated, true); foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact] as $actorId) { IPS_SetVariableCustomAction($actorId, $actorScript); @@ -128,6 +146,22 @@ function enelixDemoInstall(): array 'DiagnosevariablenAnzeigen' => true, ]); + $energyDistribution = enelixDemoEnsureInstance( + $dashboard, + 'Energieverteilung', + 'Energieverteilung', + ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE, + 10 + ); + enelixDemoConfigureEnergyDistribution($energyDistribution, [ + enelixDemoEnergyNode($pvDisplay, 'PV-Anlage', 'solar'), + enelixDemoEnergyNode($grid, 'Stromnetz', 'grid'), + enelixDemoEnergyNode($base, 'Haus-Grundlast', 'consumer'), + enelixDemoEnergyNode($boiler1Power, 'Boiler 1', 'consumer', $temp1), + enelixDemoEnergyNode($boiler2Power, 'Boiler 2', 'consumer', $temp2), + enelixDemoEnergyNode($dryerPower, 'Entfeuchter', 'consumer'), + ]); + foreach ([$boiler1, $boiler2] as $boilerId) { IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0); IPS_RequestAction($boilerId, 'Maximaltemperatur', 62.0); @@ -160,15 +194,25 @@ function enelixDemoInstall(): array } 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); + enelixDemoLinkInstanceVariables($boiler1View, $boiler1, $temp1); + enelixDemoLinkInstanceVariables($boiler2View, $boiler2, $temp2); + IPS_SetHidden($boiler1, true); + IPS_SetHidden($boiler2, true); + + enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 100); + enelixDemoEnsureLink($dashboard, 'Boiler1Link', 'Boiler 1', $boiler1View, 110); + enelixDemoEnsureLink($dashboard, 'Boiler2Link', 'Boiler 2', $boiler2View, 120); + enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 130); 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); + enelixDemoConfigureArchive([ + $pvDisplay, $houseDisplay, $loadDisplay, $grid, + $boiler1Power, $boiler2Power, $dryerPower, + $pvEnergy, $houseEnergy, $gridImportEnergy, $gridExportEnergy, + $temp1, $temp2, + ]); + enelixDemoConfigureVisualization($root, $energyDistribution); enelixDemoTick('Install'); return [ @@ -234,25 +278,44 @@ function enelixDemoTick(string $source = 'Manual'): void $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); + $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; + $power = $boiler1 + $boiler2 + $dryer; + $house = $base + $power; + $grid = $house - $pv; - $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')), + $pvId = enelixDemoRequiredId($dashboard, 'PVLeistung'); + $houseId = enelixDemoRequiredId($dashboard, 'Hausverbrauch'); + $loadId = enelixDemoRequiredId($dashboard, 'Verbraucherleistung'); + SetValue($gridId, round($grid, 1)); + SetValue($pvId, round($pv, 1)); + SetValue($houseId, round($house, 1)); + SetValue($loadId, (float) $power); + SetValue(enelixDemoRequiredId($dashboard, 'Boiler1Leistung'), (float) $boiler1); + SetValue(enelixDemoRequiredId($dashboard, 'Boiler2Leistung'), (float) $boiler2); + SetValue(enelixDemoRequiredId($dashboard, 'EntfeuchterLeistung'), (float) $dryer); + + $history = enelixDemoRequiredId($root, 'Historie'); + $energyIds = [ + 'pv' => enelixDemoRequiredId($history, 'PVEnergie'), + 'house' => enelixDemoRequiredId($history, 'HausEnergie'), + 'import' => enelixDemoRequiredId($history, 'NetzbezugEnergie'), + 'export' => enelixDemoRequiredId($history, 'EinspeisungEnergie'), ]; - SetValue(enelixDemoRequiredId($dashboard, 'Energiefluss'), enelixDemoRenderFlow($state)); + if ($source === 'Timer') { + $hours = $minutes / 60.0; + enelixDemoAddEnergy($energyIds['pv'], $pv, $hours); + enelixDemoAddEnergy($energyIds['house'], $house, $hours); + enelixDemoAddEnergy($energyIds['import'], max(0.0, $grid), $hours); + enelixDemoAddEnergy($energyIds['export'], max(0.0, -$grid), $hours); + } + + $chartUpdated = enelixDemoRequiredId($technical, 'LetztesDiagrammUpdate'); + if ($source !== 'Timer' || time() - (int) GetValue($chartUpdated) >= 10) { + enelixDemoUpdateCharts($history, [$pvId, $houseId, $gridId], $energyIds); + SetValue($chartUpdated, time()); + } } function enelixDemoRenderFlow(array $state): string @@ -309,7 +372,12 @@ function enelixDemoValidateRuntime(): void 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) { + foreach ([ + ENELIX_DEMO_MANAGER_MODULE, + ENELIX_DEMO_BOILER_MODULE, + ENELIX_DEMO_SINGLE_MODULE, + ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE, + ] as $moduleId) { if (!in_array($moduleId, $available, true)) { throw new RuntimeException('Benötigtes Enelix-Modul fehlt: ' . $moduleId); } @@ -323,6 +391,7 @@ function enelixDemoEnsureProfiles(): void 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'); + enelixDemoEnsureProfile('ENELIX.Demo.Energy', 2, 0, 1000000, 0.01, 2, ' kWh'); } function enelixDemoEnsureProfile(string $name, int $type, float $min, float $max, float $step, int $digits, string $suffix): void @@ -463,6 +532,7 @@ function enelixDemoConfigureArchive(array $variableIds): void foreach ($variableIds as $variableId) { AC_SetLoggingStatus($archive, $variableId, true); AC_SetAggregationType($archive, $variableId, 0); + AC_SetGraphStatus($archive, $variableId, true); } IPS_ApplyChanges($archive); } @@ -526,7 +596,7 @@ function enelixDemoRemove(bool $confirmed = false): void IPS_ApplyChanges($visualization); } enelixDemoDeleteTree($root); - foreach (['ENELIX.Demo.Power', 'ENELIX.Demo.PowerPositive', 'ENELIX.Demo.Hour', 'ENELIX.Demo.Percent', 'ENELIX.Demo.Speed'] as $profile) { + foreach (['ENELIX.Demo.Power', 'ENELIX.Demo.PowerPositive', 'ENELIX.Demo.Hour', 'ENELIX.Demo.Percent', 'ENELIX.Demo.Speed', 'ENELIX.Demo.Energy'] as $profile) { if (IPS_VariableProfileExists($profile)) { IPS_DeleteVariableProfile($profile); } @@ -553,3 +623,174 @@ function enelixDemoDeleteTree(int $objectId): void default: throw new RuntimeException('Unbekannter Objekttyp: ' . $type); } } + + +function enelixDemoEnergyNode( + int $variableId, + string $name, + string $type, + int $infoVariableId = 0 +): array { + return [ + 'VariableConfigType' => 0, + 'VariableID' => $variableId, + 'SecondaryVariableID' => 0, + 'Name' => $name, + 'Type' => $type, + 'Grouped' => false, + 'CalculationType' => 0, + 'InfoVariableID' => $infoVariableId, + 'Invert' => false, + ]; +} + +function enelixDemoConfigureEnergyDistribution(int $instanceId, array $nodes): void +{ + enelixDemoConfigure($instanceId, [ + 'Calculation' => false, + 'Config' => json_encode($nodes, JSON_THROW_ON_ERROR), + 'EnableLimits' => false, + 'Restrictions' => '[]', + ]); +} + +function enelixDemoLinkInstanceVariables(int $categoryId, int $instanceId, int $sensorId): void +{ + enelixDemoEnsureLink($categoryId, 'SensorTemperatur', 'Gemessene Temperatur', $sensorId, 5); + $position = 10; + foreach (IPS_GetChildrenIDs($instanceId) as $childId) { + if ((int) IPS_GetObject($childId)['ObjectType'] !== 2) { + continue; + } + $object = IPS_GetObject($childId); + $ident = $object['ObjectIdent'] !== '' ? $object['ObjectIdent'] : (string) $childId; + enelixDemoEnsureLink( + $categoryId, + 'Wert_' . preg_replace('/[^A-Za-z0-9_]/', '_', $ident), + (string) $object['ObjectName'], + (int) $childId, + $position + ); + $position += 10; + } +} + +function enelixDemoAddEnergy(int $variableId, float $powerW, float $hours): void +{ + $energy = (float) GetValue($variableId) + (($powerW * $hours) / 1000.0); + SetValue($variableId, round($energy, 4)); +} + +function enelixDemoUpdateCharts(int $historyId, array $powerIds, array $energyIds): void +{ + $archive = enelixDemoFirstInstance(ENELIX_DEMO_ARCHIVE_MODULE); + if ($archive === 0) { + return; + } + $end = time(); + $start = $end - 1200; + $powerSeries = [ + ['name' => 'PV', 'color' => '#f4c542', 'values' => enelixDemoLoggedSeries($archive, $powerIds[0], $start, $end)], + ['name' => 'Hausverbrauch', 'color' => '#2bb673', 'values' => enelixDemoLoggedSeries($archive, $powerIds[1], $start, $end)], + ['name' => 'Netzleistung', 'color' => '#e95d5d', 'values' => enelixDemoLoggedSeries($archive, $powerIds[2], $start, $end)], + ]; + $energySeries = [ + ['name' => 'PV', 'color' => '#f4c542', 'values' => enelixDemoLoggedSeries($archive, $energyIds['pv'], $start, $end)], + ['name' => 'Haus', 'color' => '#2bb673', 'values' => enelixDemoLoggedSeries($archive, $energyIds['house'], $start, $end)], + ['name' => 'Netzbezug', 'color' => '#e95d5d', 'values' => enelixDemoLoggedSeries($archive, $energyIds['import'], $start, $end)], + ['name' => 'Einspeisung', 'color' => '#38a3db', 'values' => enelixDemoLoggedSeries($archive, $energyIds['export'], $start, $end)], + ]; + SetValue( + enelixDemoRequiredId($historyId, 'Leistungsdiagramm'), + enelixDemoRenderChart('Leistungsverlauf · letzte 20 Minuten', $powerSeries, 'W', $start, $end) + ); + SetValue( + enelixDemoRequiredId($historyId, 'Energiediagramm'), + enelixDemoRenderChart('Kumulierte Energie · Simulationszeit', $energySeries, 'kWh', $start, $end) + ); +} + +function enelixDemoLoggedSeries(int $archiveId, int $variableId, int $start, int $end): array +{ + $values = array_reverse(AC_GetLoggedValues($archiveId, $variableId, $start, $end, 0)); + if ($values === []) { + return [['time' => $end, 'value' => (float) GetValue($variableId)]]; + } + $step = max(1, (int) ceil(count($values) / 90)); + $result = []; + foreach ($values as $index => $value) { + if ($index % $step !== 0 && $index !== count($values) - 1) { + continue; + } + $result[] = [ + 'time' => (int) $value['TimeStamp'], + 'value' => (float) $value['Value'], + ]; + } + return $result; +} + +function enelixDemoRenderChart( + string $title, + array $series, + string $unit, + int $start, + int $end +): string { + $allValues = [0.0]; + foreach ($series as $line) { + foreach ($line['values'] as $point) { + $allValues[] = (float) $point['value']; + } + } + $min = min($allValues); + $max = max($allValues); + if (abs($max - $min) < 0.0001) { + $max = $min + 1.0; + } + + $left = 72.0; + $top = 34.0; + $width = 650.0; + $height = 210.0; + $timeRange = max(1, $end - $start); + $valueRange = $max - $min; + $grid = ''; + for ($index = 0; $index <= 4; $index++) { + $y = $top + (($height / 4) * $index); + $value = $max - (($valueRange / 4) * $index); + $grid .= ''; + $grid .= '' + . number_format($value, $unit === 'W' ? 0 : 2, ',', "'") . ''; + } + + $lines = ''; + $legend = ''; + foreach ($series as $line) { + $points = []; + foreach ($line['values'] as $point) { + $x = $left + ((((int) $point['time'] - $start) / $timeRange) * $width); + $y = $top + (($max - (float) $point['value']) / $valueRange * $height); + $points[] = round($x, 1) . ',' . round($y, 1); + } + $color = htmlspecialchars((string) $line['color'], ENT_QUOTES, 'UTF-8'); + $name = htmlspecialchars((string) $line['name'], ENT_QUOTES, 'UTF-8'); + $lines .= ''; + $legend .= '' . $name . ''; + } + + return '

' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

+ +' . $grid . '' . $lines . ' +' . date('H:i', $start) . ' +' . date('H:i', $end) . ' +' . htmlspecialchars($unit, ENT_QUOTES, 'UTF-8') . ' +
' . $legend . '
'; +} diff --git a/tests/DemoanlageBootstrapTest.php b/tests/DemoanlageBootstrapTest.php index 8110f20..4a307cf 100644 --- a/tests/DemoanlageBootstrapTest.php +++ b/tests/DemoanlageBootstrapTest.php @@ -31,6 +31,47 @@ final class DemoanlageBootstrapTest extends TestCase self::assertStringContainsString('12:30', $html); } + public function testDiagrammStelltMehrereZeitreihenDar(): void + { + $html = enelixDemoRenderChart( + 'Leistungsverlauf', + [ + ['name' => 'PV', 'color' => '#f4c542', 'values' => [ + ['time' => 100, 'value' => 1000.0], + ['time' => 200, 'value' => 3000.0], + ]], + ['name' => 'Netz', 'color' => '#e95d5d', 'values' => [ + ['time' => 100, 'value' => -500.0], + ['time' => 200, 'value' => 500.0], + ]], + ], + 'W', + 100, + 200 + ); + + self::assertStringContainsString(' 0, + 'VariableID' => 12345, + 'SecondaryVariableID' => 0, + 'Name' => 'PV-Anlage', + 'Type' => 'solar', + 'Grouped' => false, + 'CalculationType' => 0, + 'InfoVariableID' => 54321, + 'Invert' => false, + ], enelixDemoEnergyNode(12345, 'PV-Anlage', 'solar', 54321)); + } + public function testRollbackIstExplizitBestaetigungspflichtig(): void { $this->expectException(RuntimeException::class);