diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37288fd..50b2e88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
Alle wesentlichen Aenderungen an Enelix Utils werden in dieser Datei dokumentiert.
+## Unreleased
+
+- Energiediagramm zeigt wieder den urspruenglichen dunklen Energy-Pie-Hintergrund
+ mit blauen und violetten Farbverlaeufen, auch bei hellem Visualisierungsthema.
+ Bedienelemente bleiben lesbar; Konfiguration und Berechnungen sind unveraendert.
+
## 0.1 Build 1 - Erste Beta - 2026-09-29
### Enthalten
diff --git a/Energiediagramm/README.md b/Energiediagramm/README.md
index fba727d..6b3b370 100644
--- a/Energiediagramm/README.md
+++ b/Energiediagramm/README.md
@@ -31,11 +31,14 @@ dezimalen Farbwerte.
## Darstellung
Der Inhalt beginnt unterhalb des von Symcon eingeblendeten Kachelkopfs, damit
-Zeitraum und Datum vollstaendig bedienbar bleiben. Hintergrund, Text und
-Bedienelemente verwenden die von IP-Symcon bereitgestellten Kachel- und
-Textfarben. Auch geoeffnete Auswahlfelder sowie native Datumselemente passen sich
-automatisch an ein helles oder dunkles Visualisierungsthema an. Beim Oeffnen und
-beim erneuten Sichtbarwerden wird die Darstellung aktualisiert. Das
+Zeitraum und Datum vollstaendig bedienbar bleiben. Der urspruengliche Energy-Pie-
+Hintergrund mit dunkler Grundfarbe und blauen/violetten Farbverlaeufen bleibt
+auch in einem hellen Visualisierungsthema sichtbar. Text, Auswahlfelder und
+native Datumselemente verwenden innerhalb des Moduls dazu passende helle
+Schrift und dunkle Bedienflaechen. Die konfigurierten Diagrammfarben bleiben
+unveraendert; eine Konfigurationsmigration ist nicht erforderlich. Die
+Hintergrundebene blockiert weder Klicks noch Scrollen. Beim Oeffnen und beim
+erneuten Sichtbarwerden wird die Darstellung aktualisiert. Das
Diagrammraster wechselt bei geringer Breite automatisch von zwei Spalten auf
eine Spalte.
diff --git a/Energiediagramm/SDL_MIGRATION.md b/Energiediagramm/SDL_MIGRATION.md
new file mode 100644
index 0000000..cdd6e14
--- /dev/null
+++ b/Energiediagramm/SDL_MIGRATION.md
@@ -0,0 +1,18 @@
+# Nicht eindeutig bestimmbare Energieanteile
+
+Die optionale Property `EnergieanteileBerechenbar` ist standardmaessig true und
+erhaelt das bisherige Verhalten. Setzt ein aufrufender Manager sie auf false,
+bleiben gemessene Summen erhalten. Eigenverbrauch und zugehoerige Anteile werden
+als null geliefert und als nicht bestimmbar dargestellt, niemals als 0 Prozent.
+
+Anwendungsfall: Netzbezug enthaelt SDL, die Hauslast wurde um SDL bereinigt.
+Ohne eindeutige Zuordnung laesst sich der solare Anteil nicht exakt bestimmen.
+Die Policy gehoert in EMS; Utils implementiert nur die generische Darstellung.
+
+Individuelle Quellen, Farben und Energiezaehler werden nicht veraendert. Die
+bereits vorbereitete Wiederherstellung des dunklen Pie-Hintergrunds bleibt
+erhalten. Utils vor einem Manager mit dieser neuen Property aktualisieren.
+
+Validierung: 58 PHPUnit-Tests und vier zusaetzliche JavaScript-Visualisierungstests
+erfolgreich; unbekannte, alte numerische und spaeter wieder verfuegbare Anteile
+sind abgedeckt.
diff --git a/Energiediagramm/form.json b/Energiediagramm/form.json
index 80dcaa1..4b56da5 100644
--- a/Energiediagramm/form.json
+++ b/Energiediagramm/form.json
@@ -32,6 +32,15 @@
"maximum": 1000000,
"digits": 6
},
+ {
+ "type": "CheckBox",
+ "name": "EnergieanteileBerechenbar",
+ "caption": "Eigenverbrauch und Autarkie aus gemeinsamer Energiebilanz berechnen"
+ },
+ {
+ "type": "Label",
+ "caption": "Bei abweichenden Messgrenzen oder nicht zuordenbaren Speicherfluessen ausschalten. Energiesummen bleiben sichtbar; Anteile werden als nicht bestimmbar gekennzeichnet."
+ },
{
"type": "Select",
"name": "Standardzeitraum",
diff --git a/Energiediagramm/libs/EnergyChartCalculator.php b/Energiediagramm/libs/EnergyChartCalculator.php
index 0400ee1..4fe8a4a 100644
--- a/Energiediagramm/libs/EnergyChartCalculator.php
+++ b/Energiediagramm/libs/EnergyChartCalculator.php
@@ -82,16 +82,17 @@ final class EnergyChartCalculator
* Einspeisung: float,
* Netzbezug: float,
* Hausverbrauch: float,
- * Eigenverbrauch: float,
- * Eigenverbrauchsquote: float,
- * Autarkiegrad: float
+ * Eigenverbrauch: float|null,
+ * Eigenverbrauchsquote: float|null,
+ * Autarkiegrad: float|null
* }
*/
public static function metrics(
float $production,
float $feedIn,
float $grid,
- ?float $consumption = null
+ ?float $consumption = null,
+ bool $sharesAvailable = true
): array {
$production = max(0.0, $production);
$feedIn = max(0.0, $feedIn);
@@ -106,13 +107,13 @@ final class EnergyChartCalculator
'Einspeisung' => $feedIn,
'Netzbezug' => $grid,
'Hausverbrauch' => $house,
- 'Eigenverbrauch' => $selfConsumption,
- 'Eigenverbrauchsquote' => $production > 0.0
+ 'Eigenverbrauch' => $sharesAvailable ? $selfConsumption : null,
+ 'Eigenverbrauchsquote' => !$sharesAvailable ? null : ($production > 0.0
? min(100.0, ($selfConsumption / $production) * 100.0)
- : 0.0,
- 'Autarkiegrad' => $house > 0.0
+ : 0.0),
+ 'Autarkiegrad' => !$sharesAvailable ? null : ($house > 0.0
? min(100.0, ($selfConsumption / $house) * 100.0)
- : 0.0,
+ : 0.0),
];
}
diff --git a/Energiediagramm/module.html b/Energiediagramm/module.html
index 6e49f8f..204eefa 100644
--- a/Energiediagramm/module.html
+++ b/Energiediagramm/module.html
@@ -63,6 +63,7 @@
html,
body {
min-width: 0;
+ height: 100%;
min-height: 100%;
margin: 0;
color: var(--content-color, inherit);
@@ -70,6 +71,11 @@
}
#energy-app {
+ --card-color: #121216;
+ --content-color: #f5f5f7;
+ position: relative;
+ isolation: isolate;
+ color-scheme: dark;
width: 100%;
min-height: 100%;
box-sizing: border-box;
@@ -80,6 +86,20 @@
background-color: var(--card-color, transparent);
}
+ /* Keep the original Energy Pie colors behind the scrollable controls. */
+ #energy-app::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background:
+ radial-gradient(520px 420px at 18% 18%, rgba(99, 179, 255, 0.42), transparent 60%),
+ radial-gradient(560px 460px at 82% 28%, rgba(168, 85, 247, 0.38), transparent 62%),
+ radial-gradient(620px 520px at 55% 85%, rgba(99, 179, 255, 0.26), transparent 64%);
+ filter: blur(18px);
+ pointer-events: none;
+ }
+
.toolbar,
.custom-range,
.button-row,
@@ -228,6 +248,11 @@
dominant-baseline: middle;
}
+ .donut-label.unavailable {
+ font-size: 14px;
+ font-weight: 600;
+ }
+
.error {
min-height: 18px;
margin-top: 8px;
@@ -366,6 +391,8 @@
}
function donut(target, percent, color) {
+ const available = percent !== null && percent !== undefined
+ && Number.isFinite(Number(percent));
const value = Math.max(0, Math.min(100, asNumber(percent)));
const radius = 58;
const circumference = 2 * Math.PI * radius;
@@ -373,15 +400,17 @@
target.innerHTML = [
''
].join('');
}
@@ -421,19 +450,24 @@
const colors = data.colors || {};
donut(
selfConsumptionChart,
- values.Eigenverbrauchsquote,
+ data.sharesAvailable === false ? null : values.Eigenverbrauchsquote,
colors.selfConsumption || '#2F80ED'
);
donut(
autarkyChart,
- values.Autarkiegrad,
+ data.sharesAvailable === false ? null : values.Autarkiegrad,
colors.autarky || '#22A06B'
);
- message.hidden = data.hasData !== false;
- message.textContent = data.hasData === false
- ? (data.noDataHint || 'Keine Archivwerte verfügbar.')
- : '';
+ const hints = [];
+ if (data.hasData === false) {
+ hints.push(data.noDataHint || 'Keine Archivwerte verfügbar.');
+ }
+ if (data.sharesAvailable === false) {
+ hints.push(data.attributionHint || 'Eigenverbrauch und Autarkie sind nicht eindeutig bestimmbar.');
+ }
+ message.hidden = hints.length === 0;
+ message.textContent = hints.join(' ');
}
window.handleMessage = function (data) {
diff --git a/Energiediagramm/module.php b/Energiediagramm/module.php
index 63d9eab..6918d9c 100644
--- a/Energiediagramm/module.php
+++ b/Energiediagramm/module.php
@@ -25,6 +25,7 @@ class Energiediagramm extends IPSModule
$this->RegisterPropertyInteger('NetzbezugZaehlerID', 0);
$this->RegisterPropertyInteger('VerbrauchZaehlerID', 0);
$this->RegisterPropertyFloat('Zaehlerfaktor', 1.0);
+ $this->RegisterPropertyBoolean('EnergieanteileBerechenbar', true);
$this->RegisterPropertyString('Standardzeitraum', 'day');
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
@@ -253,6 +254,7 @@ class Energiediagramm extends IPSModule
$customStart,
$customEnd
);
+ $sharesAvailable = $this->ReadPropertyBoolean('EnergieanteileBerechenbar');
$payload = [
'range' => $range,
@@ -263,6 +265,9 @@ class Energiediagramm extends IPSModule
'tEnd' => $end,
'hasData' => false,
'noDataHint' => 'Energiezähler konfigurieren.',
+ 'sharesAvailable' => $sharesAvailable,
+ 'attributionHint' => $sharesAvailable ? ''
+ : 'Eigenverbrauch und Autarkie sind bei unterschiedlichen Messgrenzen nicht eindeutig bestimmbar. Die Energiesummen bleiben separat sichtbar.',
'colors' => [
'selfConsumption' => EnergyChartCalculator::normalizeColor(
$this->ReadPropertyString('FarbeEigenverbrauch'),
@@ -273,7 +278,7 @@ class Energiediagramm extends IPSModule
self::COLOR_AUTARKY
),
],
- 'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0),
+ 'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0, null, $sharesAvailable),
];
if (!$this->configurationIsValid()) {
@@ -329,7 +334,8 @@ class Energiediagramm extends IPSModule
$production['value'] * $factor,
$feedIn['value'] * $factor,
$grid['value'] * $factor,
- $consumption
+ $consumption,
+ $sharesAvailable
);
if (!$payload['hasData']) {
diff --git a/tests/EnergiediagrammModulstrukturTest.php b/tests/EnergiediagrammModulstrukturTest.php
index d0de12c..c4e8690 100644
--- a/tests/EnergiediagrammModulstrukturTest.php
+++ b/tests/EnergiediagrammModulstrukturTest.php
@@ -33,6 +33,7 @@ final class EnergiediagrammModulstrukturTest extends TestCase
'EinspeisungZaehlerID',
'NetzbezugZaehlerID',
'VerbrauchZaehlerID',
+ 'EnergieanteileBerechenbar',
'Standardzeitraum',
'FarbeEigenverbrauch',
'FarbeAutarkie',
@@ -58,6 +59,30 @@ final class EnergiediagrammModulstrukturTest extends TestCase
self::assertStringNotContainsString("addEventListener('load'", $html);
}
+ public function testOriginalEnergyPieBackgroundStaysReadableAndNonInteractive(): void
+ {
+ $html = file_get_contents(__DIR__ . '/../Energiediagramm/module.html');
+ self::assertNotFalse($html);
+ self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*--card-color: #121216;/s', $html);
+ self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*--content-color: #f5f5f7;/s', $html);
+ self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*color-scheme: dark;/s', $html);
+ self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*isolation: isolate;/s', $html);
+ self::assertSame(1, preg_match('/#energy-app::before\\s*\\{([^}]+)\\}/s', $html, $background));
+ foreach ([
+ 'inset: 0',
+ 'z-index: -1',
+ 'radial-gradient(520px 420px at 18% 18%, rgba(99, 179, 255, 0.42), transparent 60%)',
+ 'radial-gradient(560px 460px at 82% 28%, rgba(168, 85, 247, 0.38), transparent 62%)',
+ 'radial-gradient(620px 520px at 55% 85%, rgba(99, 179, 255, 0.26), transparent 64%)',
+ 'filter: blur(18px)',
+ 'pointer-events: none',
+ ] as $style) {
+ self::assertStringContainsString($style, $background[1]);
+ }
+ self::assertStringContainsString('overflow: auto', $html);
+ self::assertStringNotContainsString('touch-action: none', $html);
+ }
+
public function testModuleEmbedsInitialPayloadAndEnablesSupportedVisualization(): void
{
$module = file_get_contents(__DIR__ . '/../Energiediagramm/module.php');
diff --git a/tests/EnergiediagrammTest.php b/tests/EnergiediagrammTest.php
index 64e600e..01d7d6f 100644
--- a/tests/EnergiediagrammTest.php
+++ b/tests/EnergiediagrammTest.php
@@ -127,4 +127,28 @@ final class EnergiediagrammTest extends TestCase
EnergyChartCalculator::normalizeColor('-1', '#22a06b')
);
}
+
+ public function testMixedMeasurementBoundariesKeepTotalsWithoutInventingShares(): void
+ {
+ $values = EnergyChartCalculator::metrics(10.0, 0.0, 10.0, 5.0, false);
+
+ self::assertSame(10.0, $values['Produktion']);
+ self::assertSame(0.0, $values['Einspeisung']);
+ self::assertSame(10.0, $values['Netzbezug']);
+ self::assertSame(5.0, $values['Hausverbrauch']);
+ self::assertNull($values['Eigenverbrauch']);
+ self::assertNull($values['Eigenverbrauchsquote']);
+ self::assertNull($values['Autarkiegrad']);
+ }
+
+ public function testUnavailableSharesRemainNullWithZeroEnergy(): void
+ {
+ $unavailable = EnergyChartCalculator::metrics(0.0, 0.0, 0.0, null, false);
+ self::assertNull($unavailable['Eigenverbrauchsquote']);
+ self::assertNull($unavailable['Autarkiegrad']);
+
+ $available = EnergyChartCalculator::metrics(0.0, 0.0, 0.0);
+ self::assertSame(0.0, $available['Eigenverbrauchsquote']);
+ self::assertSame(0.0, $available['Autarkiegrad']);
+ }
}
diff --git a/tests/EnergyPieVisualizationTest.cjs b/tests/EnergyPieVisualizationTest.cjs
new file mode 100644
index 0000000..3ab9a5a
--- /dev/null
+++ b/tests/EnergyPieVisualizationTest.cjs
@@ -0,0 +1,88 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const vm = require('node:vm');
+
+function visualization(initialState) {
+ const elements = new Map();
+ const element = id => {
+ if (!elements.has(id)) {
+ elements.set(id, {
+ innerHTML: '', textContent: '', value: '', hidden: false,
+ addEventListener() {}, classList: { toggle() {} }
+ });
+ }
+ return elements.get(id);
+ };
+ const window = { setTimeout() {} };
+ const document = {
+ getElementById: element,
+ documentElement: { style: {} },
+ addEventListener() {}
+ };
+ const html = fs.readFileSync(path.join(__dirname, '../Energiediagramm/module.html'), 'utf8');
+ const script = html.match(/