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 = [ '', + + (available ? value.toFixed(1) + ' Prozent' : 'Nicht bestimmbar') + '">', '', - '', - '' - + value.toFixed(1) + '%', + + ' stroke-dasharray="' + filled + ' ' + circumference + '">' : '', + available ? '' + + value.toFixed(1) + '%' + : '' + + 'Nichtbestimmbar', '' ].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(/