This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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('<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
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
Reference in New Issue
Block a user