Enhance demo visualization and charts
Tests / test (push) Successful in 47s

This commit is contained in:
dh
2026-09-17 14:06:12 +00:00
parent 312b76aee5
commit abd8c7edeb
3 changed files with 326 additions and 33 deletions
+14 -3
View File
@@ -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), - zwei mehrstufige Boiler (1,2/2,4 kW und 1,5/3,0 kW),
- einen einstufigen Entfeuchter (0,9 kW), - einen einstufigen Entfeuchter (0,9 kW),
- simulierte PV-Erzeugung, Grundlast, Netzleistung und Boilertemperaturen, - simulierte PV-Erzeugung, Grundlast, Netzleistung und Boilertemperaturen,
- Kachelvisualisierung mit Energiefluss und bedienbaren Simulationswerten, - native Symcon-Instanz `Energy Distribution` für den Energiefluss,
- Archivierung der wichtigsten Leistungs- und Temperaturwerte. - 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 ## Installation
@@ -31,12 +34,20 @@ Unter `Simulation` lassen sich Tageslauf, Tageszeit, Geschwindigkeit,
PV-Spitzenleistung, Bewölkung und Haus-Grundlast verändern. Der Manager verteilt 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. 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 ## Validierung
- Alle vier Enelix-Instanzen müssen Status 102 besitzen. - Alle vier Enelix-Instanzen müssen Status 102 besitzen.
- Der Manager muss drei Verbraucher melden. - Der Manager muss drei Verbraucher melden.
- Bei ausreichendem PV-Überschuss müssen Verbraucher schalten. - 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. - Ein erneuter Installationslauf muss dieselbe Root-ID zurückgeben.
## Rollback ## Rollback
+271 -30
View File
@@ -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_SINGLE_MODULE = '{15879A4E-D0C2-4495-83DE-46E1E462591E}';
const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}'; const ENELIX_DEMO_TILE_MODULE = '{B5B875BB-9B76-45FD-4E67-2607E45B3AC4}';
const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}'; const ENELIX_DEMO_ARCHIVE_MODULE = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
const ENELIX_DEMO_ENERGY_DISTRIBUTION_MODULE = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
function enelixDemoInstall(): array function enelixDemoInstall(): array
{ {
@@ -16,17 +17,32 @@ function enelixDemoInstall(): array
$root = enelixDemoEnsureCategory(0, ENELIX_DEMO_ROOT_IDENT, 'Enelix 2 Demoanlage', 100); $root = enelixDemoEnsureCategory(0, ENELIX_DEMO_ROOT_IDENT, 'Enelix 2 Demoanlage', 100);
IPS_SetInfo($root, 'Idempotent erzeugte Enelix-2-Demo. Entfernen nur mit enelixDemoRemove(true).'); 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); $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); $technical = enelixDemoEnsureCategory($root, 'Technik', 'Simulationstechnik', 90, true);
enelixDemoEnsureProfiles(); 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); $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); $houseDisplay = enelixDemoEnsureVariable($dashboard, 'Hausverbrauch', 'Hausverbrauch gesamt', 2, 'ENELIX.Demo.PowerPositive', 0.0, 30);
$grid = enelixDemoEnsureVariable($dashboard, 'Netzleistung', 'Netzleistung', 2, 'ENELIX.Demo.Power', 0.0, 40); $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); $auto = enelixDemoEnsureVariable($simulation, 'Automatik', 'Tageslauf automatisch', 0, '~Switch', true, 10);
$hour = enelixDemoEnsureVariable($simulation, 'Tageszeit', 'Simulierte Tageszeit', 2, 'ENELIX.Demo.Hour', 8.0, 20); $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); $boiler21 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe1', 'Boiler 2 Stufe 1', 0, '~Switch', false, 140);
$boiler22 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe2', 'Boiler 2 Stufe 2', 0, '~Switch', false, 150); $boiler22 = enelixDemoEnsureVariable($technical, 'Boiler2Stufe2', 'Boiler 2 Stufe 2', 0, '~Switch', false, 150);
$dryContact = enelixDemoEnsureVariable($technical, 'EntfeuchterKontakt', 'Entfeuchter Kontakt', 0, '~Switch', false, 160); $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) { foreach ([$boiler11, $boiler12, $boiler21, $boiler22, $dryContact] as $actorId) {
IPS_SetVariableCustomAction($actorId, $actorScript); IPS_SetVariableCustomAction($actorId, $actorScript);
@@ -128,6 +146,22 @@ function enelixDemoInstall(): array
'DiagnosevariablenAnzeigen' => true, '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) { foreach ([$boiler1, $boiler2] as $boilerId) {
IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0); IPS_RequestAction($boilerId, 'Mindesttemperatur', 42.0);
IPS_RequestAction($boilerId, 'Maximaltemperatur', 62.0); IPS_RequestAction($boilerId, 'Maximaltemperatur', 62.0);
@@ -160,15 +194,25 @@ function enelixDemoInstall(): array
} }
IPS_RequestAction($manager, 'Aktiv', true); IPS_RequestAction($manager, 'Aktiv', true);
enelixDemoEnsureLink($dashboard, 'ManagerLink', 'Manager', $manager, 50); enelixDemoLinkInstanceVariables($boiler1View, $boiler1, $temp1);
enelixDemoEnsureLink($dashboard, 'Boiler1Link', 'Boiler 1', $boiler1, 60); enelixDemoLinkInstanceVariables($boiler2View, $boiler2, $temp2);
enelixDemoEnsureLink($dashboard, 'Boiler2Link', 'Boiler 2', $boiler2, 70); IPS_SetHidden($boiler1, true);
enelixDemoEnsureLink($dashboard, 'EntfeuchterLink', 'Entfeuchter', $dryer, 80); 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, 'Boiler1TemperaturLink', 'Boiler 1 Temperatur', $temp1, 70);
enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80); enelixDemoEnsureLink($simulation, 'Boiler2TemperaturLink', 'Boiler 2 Temperatur', $temp2, 80);
enelixDemoConfigureArchive([$pvDisplay, $loadDisplay, $grid, $temp1, $temp2]); enelixDemoConfigureArchive([
enelixDemoConfigureVisualization($root, $flow); $pvDisplay, $houseDisplay, $loadDisplay, $grid,
$boiler1Power, $boiler2Power, $dryerPower,
$pvEnergy, $houseEnergy, $gridImportEnergy, $gridExportEnergy,
$temp1, $temp2,
]);
enelixDemoConfigureVisualization($root, $energyDistribution);
enelixDemoTick('Install'); enelixDemoTick('Install');
return [ return [
@@ -234,25 +278,44 @@ function enelixDemoTick(string $source = 'Manual'): void
$manager = enelixDemoRequiredId($ems, 'Manager'); $manager = enelixDemoRequiredId($ems, 'Manager');
IPS_RequestAction($manager, 'Regeln', true); IPS_RequestAction($manager, 'Regeln', true);
$power = enelixDemoControlledPower($contacts); $boiler1 = (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0);
$grid = $base + $power - $pv; $boiler2 = (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0);
SetValue($gridId, round($grid, 1)); $dryer = (bool) GetValue($contacts['dryer']) ? 900 : 0;
SetValue(enelixDemoRequiredId($dashboard, 'PVLeistung'), round($pv, 1)); $power = $boiler1 + $boiler2 + $dryer;
SetValue(enelixDemoRequiredId($dashboard, 'Verbraucherleistung'), (float) $power); $house = $base + $power;
$grid = $house - $pv;
$state = [ $pvId = enelixDemoRequiredId($dashboard, 'PVLeistung');
'hour' => $hour, $houseId = enelixDemoRequiredId($dashboard, 'Hausverbrauch');
'pv' => $pv, $loadId = enelixDemoRequiredId($dashboard, 'Verbraucherleistung');
'base' => $base, SetValue($gridId, round($grid, 1));
'grid' => $grid, SetValue($pvId, round($pv, 1));
'boiler1' => (bool) GetValue($contacts['boiler12']) ? 2400 : ((bool) GetValue($contacts['boiler11']) ? 1200 : 0), SetValue($houseId, round($house, 1));
'boiler2' => (bool) GetValue($contacts['boiler22']) ? 3000 : ((bool) GetValue($contacts['boiler21']) ? 1500 : 0), SetValue($loadId, (float) $power);
'dryer' => (bool) GetValue($contacts['dryer']) ? 900 : 0, SetValue(enelixDemoRequiredId($dashboard, 'Boiler1Leistung'), (float) $boiler1);
'temp1' => (float) GetValue($temp1), SetValue(enelixDemoRequiredId($dashboard, 'Boiler2Leistung'), (float) $boiler2);
'temp2' => (float) GetValue($temp2), SetValue(enelixDemoRequiredId($dashboard, 'EntfeuchterLeistung'), (float) $dryer);
'manager' => (string) GetValue(enelixDemoRequiredId($manager, 'Betriebsart')),
$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 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 . '.'); throw new RuntimeException('Die Demo benötigt IP-Symcon 8.x; aktiv ist ' . $version . '.');
} }
$available = IPS_GetModuleList(); $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)) { if (!in_array($moduleId, $available, true)) {
throw new RuntimeException('Benötigtes Enelix-Modul fehlt: ' . $moduleId); 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.Hour', 2, 0, 23.75, 0.25, 2, ' h');
enelixDemoEnsureProfile('ENELIX.Demo.Percent', 1, 0, 100, 5, 0, ' %'); enelixDemoEnsureProfile('ENELIX.Demo.Percent', 1, 0, 100, 5, 0, ' %');
enelixDemoEnsureProfile('ENELIX.Demo.Speed', 1, 1, 60, 1, 0, ' min'); 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 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) { foreach ($variableIds as $variableId) {
AC_SetLoggingStatus($archive, $variableId, true); AC_SetLoggingStatus($archive, $variableId, true);
AC_SetAggregationType($archive, $variableId, 0); AC_SetAggregationType($archive, $variableId, 0);
AC_SetGraphStatus($archive, $variableId, true);
} }
IPS_ApplyChanges($archive); IPS_ApplyChanges($archive);
} }
@@ -526,7 +596,7 @@ function enelixDemoRemove(bool $confirmed = false): void
IPS_ApplyChanges($visualization); IPS_ApplyChanges($visualization);
} }
enelixDemoDeleteTree($root); 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)) { if (IPS_VariableProfileExists($profile)) {
IPS_DeleteVariableProfile($profile); IPS_DeleteVariableProfile($profile);
} }
@@ -553,3 +623,174 @@ function enelixDemoDeleteTree(int $objectId): void
default: throw new RuntimeException('Unbekannter Objekttyp: ' . $type); 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 .= '<line x1="' . $left . '" y1="' . $y . '" x2="' . ($left + $width) . '" y2="' . $y . '" />';
$grid .= '<text x="' . ($left - 10) . '" y="' . ($y + 4) . '" text-anchor="end">'
. number_format($value, $unit === 'W' ? 0 : 2, ',', "'") . '</text>';
}
$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 .= '<polyline points="' . implode(' ', $points) . '" stroke="' . $color . '" />';
$legend .= '<span><i style="background:' . $color . '"></i>' . $name . '</span>';
}
return '<style>
.enelix-chart{font-family:Inter,Segoe UI,Arial,sans-serif;background:#10151c;color:#f4f7fa;padding:14px;border-radius:6px}
.enelix-chart h3{font-size:17px;margin:0 0 8px}.enelix-chart svg{display:block;width:100%;height:auto;min-height:230px}
.enelix-chart .grid line{stroke:#33404d;stroke-width:1}.enelix-chart text{fill:#9dacbd;font-size:11px}
.enelix-chart polyline{fill:none;stroke-width:3;stroke-linejoin:round;stroke-linecap:round}
.enelix-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:4px;color:#c5ced8;font-size:12px}
.enelix-legend span{display:flex;align-items:center;gap:6px}.enelix-legend i{display:inline-block;width:18px;height:3px}
</style><div class="enelix-chart"><h3>' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</h3>
<svg viewBox="0 0 760 280" role="img" aria-label="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '">
<g class="grid">' . $grid . '</g>' . $lines . '
<text x="' . $left . '" y="266">' . date('H:i', $start) . '</text>
<text x="' . ($left + $width) . '" y="266" text-anchor="end">' . date('H:i', $end) . '</text>
<text x="18" y="20">' . htmlspecialchars($unit, ENT_QUOTES, 'UTF-8') . '</text>
</svg><div class="enelix-legend">' . $legend . '</div></div>';
}
+41
View File
@@ -31,6 +31,47 @@ final class DemoanlageBootstrapTest extends TestCase
self::assertStringContainsString('12:30', $html); 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('<svg', $html);
self::assertStringContainsString('Leistungsverlauf', $html);
self::assertStringContainsString('PV', $html);
self::assertStringContainsString('Netz', $html);
self::assertSame(2, substr_count($html, '<polyline'));
}
public function testNativeEnergieflussKnotenHatVollstaendigesSchema(): void
{
self::assertSame([
'VariableConfigType' => 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 public function testRollbackIstExplizitBestaetigungspflichtig(): void
{ {
$this->expectException(RuntimeException::class); $this->expectException(RuntimeException::class);