fix(energy-pie): represent indeterminate shares and preserve prepared styling
Tests / test (push) Successful in 50s

This commit is contained in:
dh
2026-10-06 17:40:42 +00:00
parent c5f924bd19
commit 5b6125d086
10 changed files with 241 additions and 27 deletions
+8 -5
View File
@@ -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.
+18
View File
@@ -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.
+9
View File
@@ -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",
+10 -9
View File
@@ -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),
];
}
+45 -11
View File
@@ -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 = [
'<svg viewBox="0 0 176 176" role="img" aria-label="'
+ value.toFixed(1) + ' Prozent">',
+ (available ? value.toFixed(1) + ' Prozent' : 'Nicht bestimmbar') + '">',
'<circle class="donut-track" cx="88" cy="88" r="' + radius
+ '" fill="none" stroke-width="18"></circle>',
'<circle class="donut-value" cx="88" cy="88" r="' + radius
available ? '<circle class="donut-value" cx="88" cy="88" r="' + radius
+ '" fill="none" stroke="' + color + '" stroke-width="18"'
+ ' stroke-linecap="round" transform="rotate(-90 88 88)"'
+ ' stroke-dasharray="' + filled + ' ' + circumference + '"></circle>',
'<text class="donut-label" x="88" y="91">'
+ value.toFixed(1) + '%</text>',
+ ' stroke-dasharray="' + filled + ' ' + circumference + '"></circle>' : '',
available ? '<text class="donut-label" x="88" y="91">'
+ value.toFixed(1) + '%</text>'
: '<text class="donut-label unavailable" x="88" y="80">'
+ '<tspan x="88">Nicht</tspan><tspan x="88" dy="20">bestimmbar</tspan></text>',
'</svg>'
].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) {
+8 -2
View File
@@ -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']) {