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
+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_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 .= '<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>';
}