feat(energy-pie): finish configurable colors and Symcon 8 migration
Tests / test (push) Waiting to run
Tests / test (push) Waiting to run
This commit is contained in:
@@ -66,18 +66,42 @@
|
|||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "SelectColor",
|
"type": "SelectColor",
|
||||||
"name": "FarbeEigenverbrauch",
|
"name": "FarbeEigenverbrauchRGB",
|
||||||
"caption": "Eigenverbrauchsquote",
|
"caption": "Eigenverbrauchsquote",
|
||||||
"allowTransparent": false
|
"allowTransparent": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SelectColor",
|
"type": "SelectColor",
|
||||||
"name": "FarbeAutarkie",
|
"name": "FarbeAutarkieRGB",
|
||||||
"caption": "Autarkiegrad",
|
"caption": "Autarkiegrad",
|
||||||
"allowTransparent": false
|
"allowTransparent": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Hintergrundfarben",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "SelectColor",
|
||||||
|
"name": "FarbeHintergrundRGB",
|
||||||
|
"caption": "Grundfarbe",
|
||||||
|
"allowTransparent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectColor",
|
||||||
|
"name": "FarbeHintergrundAkzent1RGB",
|
||||||
|
"caption": "Verlaufsfarbe 1 (links und unten)",
|
||||||
|
"allowTransparent": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectColor",
|
||||||
|
"name": "FarbeHintergrundAkzent2RGB",
|
||||||
|
"caption": "Verlaufsfarbe 2 (rechts)",
|
||||||
|
"allowTransparent": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "CheckBox",
|
"type": "CheckBox",
|
||||||
"name": "Debug",
|
"name": "Debug",
|
||||||
|
|||||||
+29
-19
@@ -73,6 +73,8 @@
|
|||||||
#energy-app {
|
#energy-app {
|
||||||
--card-color: #121216;
|
--card-color: #121216;
|
||||||
--content-color: #f5f5f7;
|
--content-color: #f5f5f7;
|
||||||
|
--background-accent-1-rgb: 99, 179, 255;
|
||||||
|
--background-accent-2-rgb: 168, 85, 247;
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
@@ -86,16 +88,16 @@
|
|||||||
background-color: var(--card-color, transparent);
|
background-color: var(--card-color, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the original Energy Pie colors behind the scrollable controls. */
|
/* Keep the configurable background behind the scrollable controls. */
|
||||||
#energy-app::before {
|
#energy-app::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background:
|
background:
|
||||||
radial-gradient(520px 420px at 18% 18%, rgba(99, 179, 255, 0.42), transparent 60%),
|
radial-gradient(520px 420px at 18% 18%, rgba(var(--background-accent-1-rgb), 0.42), transparent 60%),
|
||||||
radial-gradient(560px 460px at 82% 28%, rgba(168, 85, 247, 0.38), transparent 62%),
|
radial-gradient(560px 460px at 82% 28%, rgba(var(--background-accent-2-rgb), 0.38), transparent 62%),
|
||||||
radial-gradient(620px 520px at 55% 85%, rgba(99, 179, 255, 0.26), transparent 64%);
|
radial-gradient(620px 520px at 55% 85%, rgba(var(--background-accent-1-rgb), 0.26), transparent 64%);
|
||||||
filter: blur(18px);
|
filter: blur(18px);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -309,21 +311,29 @@
|
|||||||
const selfConsumptionChart = document.getElementById('self-consumption-chart');
|
const selfConsumptionChart = document.getElementById('self-consumption-chart');
|
||||||
const autarkyChart = document.getElementById('autarky-chart');
|
const autarkyChart = document.getElementById('autarky-chart');
|
||||||
|
|
||||||
function syncColorScheme() {
|
function colorRgb(color) {
|
||||||
const cardColor = getComputedStyle(document.documentElement)
|
const value = Number.parseInt(color.slice(1), 16);
|
||||||
.getPropertyValue('--card-color')
|
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
||||||
.trim();
|
|
||||||
const match = cardColor.match(/^#?([0-9a-f]{6})$/i);
|
|
||||||
if (!match) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = Number.parseInt(match[1], 16);
|
function syncBackground(colors) {
|
||||||
const red = (value >> 16) & 255;
|
const safeColor = (value, fallback) => typeof value === 'string'
|
||||||
const green = (value >> 8) & 255;
|
&& /^#[0-9a-f]{6}$/i.test(value) ? value : fallback;
|
||||||
const blue = value & 255;
|
const background = safeColor(colors.background, '#121216');
|
||||||
const luminance = ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)) / 255;
|
const accent1 = safeColor(colors.backgroundAccent1, '#63B3FF');
|
||||||
document.documentElement.style.colorScheme = luminance < 0.5 ? 'dark' : 'light';
|
const accent2 = safeColor(colors.backgroundAccent2, '#A855F7');
|
||||||
|
app.style.setProperty('--card-color', background);
|
||||||
|
app.style.setProperty('--background-accent-1-rgb', colorRgb(accent1).join(', '));
|
||||||
|
app.style.setProperty('--background-accent-2-rgb', colorRgb(accent2).join(', '));
|
||||||
|
|
||||||
|
const linear = colorRgb(background).map(channel => {
|
||||||
|
const value = channel / 255;
|
||||||
|
return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
const luminance = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
||||||
|
const dark = luminance < 0.179;
|
||||||
|
app.style.setProperty('--content-color', dark ? '#f5f5f7' : '#121216');
|
||||||
|
app.style.colorScheme = dark ? 'dark' : 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(action, value) {
|
function send(action, value) {
|
||||||
@@ -425,7 +435,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncColorScheme();
|
const colors = data.colors || {};
|
||||||
|
syncBackground(colors);
|
||||||
range.value = data.range || 'day';
|
range.value = data.range || 'day';
|
||||||
date.value = data.date || '';
|
date.value = data.date || '';
|
||||||
customStart.value = data.customStart || '';
|
customStart.value = data.customStart || '';
|
||||||
@@ -447,7 +458,6 @@
|
|||||||
metric('Einspeisung', values.Einspeisung)
|
metric('Einspeisung', values.Einspeisung)
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
const colors = data.colors || {};
|
|
||||||
donut(
|
donut(
|
||||||
selfConsumptionChart,
|
selfConsumptionChart,
|
||||||
data.sharesAvailable === false ? null : values.Eigenverbrauchsquote,
|
data.sharesAvailable === false ? null : values.Eigenverbrauchsquote,
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ class Energiediagramm extends IPSModule
|
|||||||
private const ATTR_CUSTOM_END = 'CustomEnd';
|
private const ATTR_CUSTOM_END = 'CustomEnd';
|
||||||
private const COLOR_SELF_CONSUMPTION = '#2F80ED';
|
private const COLOR_SELF_CONSUMPTION = '#2F80ED';
|
||||||
private const COLOR_AUTARKY = '#22A06B';
|
private const COLOR_AUTARKY = '#22A06B';
|
||||||
|
private const COLOR_BACKGROUND = '#121216';
|
||||||
|
private const COLOR_BACKGROUND_ACCENT_1 = '#63B3FF';
|
||||||
|
private const COLOR_BACKGROUND_ACCENT_2 = '#A855F7';
|
||||||
|
private const COLOR_PROPERTIES = [
|
||||||
|
'FarbeEigenverbrauch' => self::COLOR_SELF_CONSUMPTION,
|
||||||
|
'FarbeAutarkie' => self::COLOR_AUTARKY,
|
||||||
|
'FarbeHintergrund' => self::COLOR_BACKGROUND,
|
||||||
|
'FarbeHintergrundAkzent1' => self::COLOR_BACKGROUND_ACCENT_1,
|
||||||
|
'FarbeHintergrundAkzent2' => self::COLOR_BACKGROUND_ACCENT_2,
|
||||||
|
];
|
||||||
|
|
||||||
public function Create(): void
|
public function Create(): void
|
||||||
{
|
{
|
||||||
@@ -30,6 +40,13 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
|
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
|
||||||
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
|
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
|
||||||
$this->RegisterPropertyString('FarbeAutarkie', self::COLOR_AUTARKY);
|
$this->RegisterPropertyString('FarbeAutarkie', self::COLOR_AUTARKY);
|
||||||
|
$this->RegisterPropertyString('FarbeHintergrund', self::COLOR_BACKGROUND);
|
||||||
|
$this->RegisterPropertyString('FarbeHintergrundAkzent1', self::COLOR_BACKGROUND_ACCENT_1);
|
||||||
|
$this->RegisterPropertyString('FarbeHintergrundAkzent2', self::COLOR_BACKGROUND_ACCENT_2);
|
||||||
|
// Keep legacy string properties: changing their type discards saved colors in Symcon 8.
|
||||||
|
foreach (self::COLOR_PROPERTIES as $property => $default) {
|
||||||
|
$this->RegisterPropertyInteger($property . 'RGB', (int) hexdec(substr($default, 1)));
|
||||||
|
}
|
||||||
$this->RegisterPropertyBoolean('Debug', false);
|
$this->RegisterPropertyBoolean('Debug', false);
|
||||||
|
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
@@ -37,6 +54,7 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->RegisterAttributeString(self::ATTR_DATE, $today);
|
$this->RegisterAttributeString(self::ATTR_DATE, $today);
|
||||||
$this->RegisterAttributeString(self::ATTR_CUSTOM_START, $today);
|
$this->RegisterAttributeString(self::ATTR_CUSTOM_START, $today);
|
||||||
$this->RegisterAttributeString(self::ATTR_CUSTOM_END, $today);
|
$this->RegisterAttributeString(self::ATTR_CUSTOM_END, $today);
|
||||||
|
$this->RegisterAttributeString('ColorConfiguration', '{}');
|
||||||
|
|
||||||
$visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=')
|
$visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=')
|
||||||
? 2
|
? 2
|
||||||
@@ -53,6 +71,10 @@ class Energiediagramm extends IPSModule
|
|||||||
{
|
{
|
||||||
parent::ApplyChanges();
|
parent::ApplyChanges();
|
||||||
|
|
||||||
|
if ($this->synchronizeColorProperties()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$today = date('Y-m-d');
|
$today = date('Y-m-d');
|
||||||
$defaultRange = $this->ReadPropertyString('Standardzeitraum');
|
$defaultRange = $this->ReadPropertyString('Standardzeitraum');
|
||||||
if (!in_array($defaultRange, ['day', 'week', 'month', 'year', 'total'], true)) {
|
if (!in_array($defaultRange, ['day', 'week', 'month', 'year', 'total'], true)) {
|
||||||
@@ -84,6 +106,51 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->recalculateAndPush();
|
$this->recalculateAndPush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function synchronizeColorProperties(): bool
|
||||||
|
{
|
||||||
|
$previous = json_decode($this->ReadAttributeString('ColorConfiguration'), true);
|
||||||
|
$previous = is_array($previous) ? $previous : [];
|
||||||
|
$state = [];
|
||||||
|
$updates = [];
|
||||||
|
|
||||||
|
foreach (self::COLOR_PROPERTIES as $property => $default) {
|
||||||
|
$legacy = $this->ReadPropertyString($property);
|
||||||
|
$rgb = $this->ReadPropertyInteger($property . 'RGB');
|
||||||
|
$old = $previous[$property] ?? null;
|
||||||
|
$rgbChanged = $rgb !== ($old['rgb'] ?? (int) hexdec(substr($default, 1)));
|
||||||
|
$legacyChanged = $legacy !== ($old['legacy'] ?? null);
|
||||||
|
|
||||||
|
// The native picker wins simultaneous edits; old scripts can still set hex strings.
|
||||||
|
if ($rgbChanged && $rgb >= 0 && $rgb <= 0xFFFFFF) {
|
||||||
|
$legacy = sprintf('#%06X', $rgb);
|
||||||
|
} elseif (!$rgbChanged && $legacyChanged) {
|
||||||
|
$normalized = EnergyChartCalculator::normalizeColor($legacy, '');
|
||||||
|
if ($normalized !== '') {
|
||||||
|
$rgb = (int) hexdec(substr($normalized, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($legacy !== $this->ReadPropertyString($property)) {
|
||||||
|
$updates[$property] = $legacy;
|
||||||
|
}
|
||||||
|
if ($rgb !== $this->ReadPropertyInteger($property . 'RGB')) {
|
||||||
|
$updates[$property . 'RGB'] = $rgb;
|
||||||
|
}
|
||||||
|
$state[$property] = ['legacy' => $legacy, 'rgb' => $rgb];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($updates as $property => $value) {
|
||||||
|
IPS_SetProperty($this->InstanceID, $property, $value);
|
||||||
|
}
|
||||||
|
$this->WriteAttributeString('ColorConfiguration', json_encode($state, JSON_THROW_ON_ERROR));
|
||||||
|
if ($updates === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPS_ApplyChanges($this->InstanceID);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public function GetVisualizationTile(): string
|
public function GetVisualizationTile(): string
|
||||||
{
|
{
|
||||||
$path = __DIR__ . '/module.html';
|
$path = __DIR__ . '/module.html';
|
||||||
@@ -277,6 +344,18 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->ReadPropertyString('FarbeAutarkie'),
|
$this->ReadPropertyString('FarbeAutarkie'),
|
||||||
self::COLOR_AUTARKY
|
self::COLOR_AUTARKY
|
||||||
),
|
),
|
||||||
|
'background' => EnergyChartCalculator::normalizeColor(
|
||||||
|
$this->ReadPropertyString('FarbeHintergrund'),
|
||||||
|
self::COLOR_BACKGROUND
|
||||||
|
),
|
||||||
|
'backgroundAccent1' => EnergyChartCalculator::normalizeColor(
|
||||||
|
$this->ReadPropertyString('FarbeHintergrundAkzent1'),
|
||||||
|
self::COLOR_BACKGROUND_ACCENT_1
|
||||||
|
),
|
||||||
|
'backgroundAccent2' => EnergyChartCalculator::normalizeColor(
|
||||||
|
$this->ReadPropertyString('FarbeHintergrundAkzent2'),
|
||||||
|
self::COLOR_BACKGROUND_ACCENT_2
|
||||||
|
),
|
||||||
],
|
],
|
||||||
'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0, null, $sharesAvailable),
|
'values' => EnergyChartCalculator::metrics(0.0, 0.0, 0.0, null, $sharesAvailable),
|
||||||
];
|
];
|
||||||
@@ -435,6 +514,14 @@ class Energiediagramm extends IPSModule
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (self::COLOR_PROPERTIES as $property => $default) {
|
||||||
|
$rgb = $this->ReadPropertyInteger($property . 'RGB');
|
||||||
|
if ($rgb < 0 || $rgb > 0xFFFFFF
|
||||||
|
|| EnergyChartCalculator::normalizeColor($this->ReadPropertyString($property), '') === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this->ReadPropertyFloat('Zaehlerfaktor') > 0.0
|
return $this->ReadPropertyFloat('Zaehlerfaktor') > 0.0
|
||||||
&& $this->ReadPropertyInteger('Aktualisierungsintervall') >= 10
|
&& $this->ReadPropertyInteger('Aktualisierungsintervall') >= 10
|
||||||
&& $this->ReadPropertyInteger('Aktualisierungsintervall') <= 3600
|
&& $this->ReadPropertyInteger('Aktualisierungsintervall') <= 3600
|
||||||
@@ -442,15 +529,7 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->ReadPropertyString('Standardzeitraum'),
|
$this->ReadPropertyString('Standardzeitraum'),
|
||||||
['day', 'week', 'month', 'year', 'total'],
|
['day', 'week', 'month', 'year', 'total'],
|
||||||
true
|
true
|
||||||
)
|
);
|
||||||
&& EnergyChartCalculator::normalizeColor(
|
|
||||||
$this->ReadPropertyString('FarbeEigenverbrauch'),
|
|
||||||
''
|
|
||||||
) !== ''
|
|
||||||
&& EnergyChartCalculator::normalizeColor(
|
|
||||||
$this->ReadPropertyString('FarbeAutarkie'),
|
|
||||||
''
|
|
||||||
) !== '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function requiredSourceIdsAreEmpty(): bool
|
private function requiredSourceIdsAreEmpty(): bool
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Energy Pie: Hintergrundfarben und Symcon-8-Farbauswahl
|
||||||
|
|
||||||
|
## Umfang und Migration
|
||||||
|
|
||||||
|
Auf ausdruecklichen Auftrag von Daniel Haefliger werden die bereits vorbereiteten,
|
||||||
|
noch unveroeffentlichten Energy-Pie-Aenderungen weitergefuehrt. Die Grundfarbe und
|
||||||
|
zwei Verlaufsfarben sind einstellbar. Text und native Bedienelemente folgen der
|
||||||
|
Helligkeit der Grundfarbe. Bestehende Standardfarben bleiben erhalten.
|
||||||
|
|
||||||
|
Die fuenf Farbauswahlen verwenden Integer-Properties mit dem Suffix `RGB`.
|
||||||
|
Die bisherigen String-Properties bleiben kompatibel und werden beim Anwenden
|
||||||
|
bidirektional synchronisiert. Bei gleichzeitiger Aenderung gewinnt die numerische
|
||||||
|
Farbauswahl. Gespeicherte Farben, Energiezaehler, Archivdaten und nicht betroffene
|
||||||
|
Einstellungen werden nicht zurueckgesetzt. Ungueltige Farben erzeugen einen
|
||||||
|
Konfigurationsfehler; die Darstellung verwendet einen sicheren Standard.
|
||||||
|
|
||||||
|
## Erneute Pruefung am 7. Oktober 2026
|
||||||
|
|
||||||
|
- Utils: 58 PHPUnit-Tests mit 365 Assertions bestanden.
|
||||||
|
- Energy Pie: sieben JavaScript-Tests bestanden.
|
||||||
|
- Geaenderte PHP-Dateien: Syntaxpruefung erfolgreich; Diff ohne Formatfehler.
|
||||||
|
- Chromium: helle und dunkle Darstellung jeweils bei 1280, 390 und 320 Pixeln;
|
||||||
|
kein horizontaler Ueberlauf, Bedienelemente erreichbar, Farbwechsel wirksam.
|
||||||
|
- EMS: 360 PHPUnit-Tests mit 1771 Assertions bestanden.
|
||||||
|
- Forecast-Formular 71, Manager-Aktionen 26, Regeltest 96 und Empfaenger 32
|
||||||
|
isolierte Pruefungen bestanden. Diese ersetzen keine Anlagenabnahme.
|
||||||
|
|
||||||
|
Die bestehenden Nachweise der Symcon-8-Farbauswahl/Migration stammen aus der
|
||||||
|
vorherigen Entwicklungsinstallation, nicht aus einer neuen Lihrenmoos-Abnahme.
|
||||||
|
Weitere offene Dokumentationsaenderungen werden nicht mit diesem Release
|
||||||
|
veroeffentlicht.
|
||||||
|
|
||||||
|
## Abgrenzung zum Forecast-/SDL-Rollout
|
||||||
|
|
||||||
|
EMS `372b997` enthaelt bereits die Forecast-Bedienintegration, die Portaldiagramme
|
||||||
|
und SDL-Anzeigen. Auf Lihrenmoos stimmen ManagerSDLTrait,
|
||||||
|
ManagerPrognoseFormular und ManagerEnergieTrait mit diesem Quellstand ueberein.
|
||||||
|
Die hier freigegebene neue Farbauswahl ist dort noch nicht installiert.
|
||||||
|
|
||||||
|
Die oeffentlichen Portaldateien wurden erneut per HTTP 200 und Dateihash
|
||||||
|
bestaetigt. Fuer die vollstaendige gespeicherte Eingangsprognose fehlt weiterhin
|
||||||
|
der privilegierte Backend-Rollout; dessen erneute Vorpruefung war erfolgreich.
|
||||||
|
Der Agent besitzt auf enelix-services keinen Docker-Socketzugriff.
|
||||||
|
|
||||||
|
Die gespeicherte Anlagenkonfiguration zeigt alten Netzfahrplan EIN und neue
|
||||||
|
Netzfahrplansteuerung AUS. Die alte Testfreigabe endete am 7. Oktober 2026 um
|
||||||
|
17:38:07 Europe/Zurich. Ein kontrollierter Symcon-Neustart nach frischer Sicherung,
|
||||||
|
automatische Timerpruefung, physischer Soll-/Ist-Nachweis, Planwechsel und sicherer
|
||||||
|
Stopp sowie unabhaengiger Geraeteausfallschutz bleiben Betriebsvoraussetzungen.
|
||||||
|
Dieser Release veraendert keine Stellfreigaben und aktiviert keinen Dauerbetrieb.
|
||||||
|
|
||||||
|
## Pull-Request-Text
|
||||||
|
|
||||||
|
Vorbereitete Energy-Pie-Hintergrundfarben abschliessen und Symcon-8-Farbauswahl
|
||||||
|
verlustfrei mit bisherigen Farbproperties synchronisieren. Regressionstests und
|
||||||
|
responsive Browserpruefung erfolgreich. Bestehende Forecast-/SDL-Integration
|
||||||
|
beibehalten; Anlagenrollout und offene Betriebsfreigabe getrennt dokumentieren.
|
||||||
@@ -37,10 +37,29 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
'Standardzeitraum',
|
'Standardzeitraum',
|
||||||
'FarbeEigenverbrauch',
|
'FarbeEigenverbrauch',
|
||||||
'FarbeAutarkie',
|
'FarbeAutarkie',
|
||||||
|
'FarbeHintergrund',
|
||||||
|
'FarbeHintergrundAkzent1',
|
||||||
|
'FarbeHintergrundAkzent2',
|
||||||
] as $property) {
|
] as $property) {
|
||||||
self::assertStringContainsString($property, $serialized);
|
self::assertStringContainsString($property, $serialized);
|
||||||
}
|
}
|
||||||
self::assertSame(2, substr_count($serialized, 'SelectColor'));
|
self::assertSame(5, substr_count($serialized, 'SelectColor'));
|
||||||
|
$colors = [];
|
||||||
|
$walk = static function (array $elements) use (&$walk, &$colors): void {
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
if ($element['type'] === 'SelectColor') {
|
||||||
|
$colors[] = $element;
|
||||||
|
}
|
||||||
|
if (isset($element['items'])) {
|
||||||
|
$walk($element['items']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$walk($form['elements']);
|
||||||
|
foreach ($colors as $color) {
|
||||||
|
self::assertStringEndsWith('RGB', $color['name']);
|
||||||
|
self::assertFalse($color['allowTransparent']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHtmlStartsWithDataAndRespondsToAvailableWidth(): void
|
public function testHtmlStartsWithDataAndRespondsToAvailableWidth(): void
|
||||||
@@ -54,7 +73,7 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('padding: 58px 10px 10px', $html);
|
self::assertStringContainsString('padding: 58px 10px 10px', $html);
|
||||||
self::assertStringContainsString('background-color: var(--card-color, transparent)', $html);
|
self::assertStringContainsString('background-color: var(--card-color, transparent)', $html);
|
||||||
self::assertStringContainsString('color: var(--content-color, CanvasText)', $html);
|
self::assertStringContainsString('color: var(--content-color, CanvasText)', $html);
|
||||||
self::assertStringContainsString('function syncColorScheme()', $html);
|
self::assertStringContainsString('function syncBackground(colors)', $html);
|
||||||
self::assertStringContainsString('SetCustomRange', $html);
|
self::assertStringContainsString('SetCustomRange', $html);
|
||||||
self::assertStringNotContainsString("addEventListener('load'", $html);
|
self::assertStringNotContainsString("addEventListener('load'", $html);
|
||||||
}
|
}
|
||||||
@@ -71,15 +90,17 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
foreach ([
|
foreach ([
|
||||||
'inset: 0',
|
'inset: 0',
|
||||||
'z-index: -1',
|
'z-index: -1',
|
||||||
'radial-gradient(520px 420px at 18% 18%, rgba(99, 179, 255, 0.42), transparent 60%)',
|
'radial-gradient(520px 420px at 18% 18%, rgba(var(--background-accent-1-rgb), 0.42), transparent 60%)',
|
||||||
'radial-gradient(560px 460px at 82% 28%, rgba(168, 85, 247, 0.38), transparent 62%)',
|
'radial-gradient(560px 460px at 82% 28%, rgba(var(--background-accent-2-rgb), 0.38), transparent 62%)',
|
||||||
'radial-gradient(620px 520px at 55% 85%, rgba(99, 179, 255, 0.26), transparent 64%)',
|
'radial-gradient(620px 520px at 55% 85%, rgba(var(--background-accent-1-rgb), 0.26), transparent 64%)',
|
||||||
'filter: blur(18px)',
|
'filter: blur(18px)',
|
||||||
'pointer-events: none',
|
'pointer-events: none',
|
||||||
] as $style) {
|
] as $style) {
|
||||||
self::assertStringContainsString($style, $background[1]);
|
self::assertStringContainsString($style, $background[1]);
|
||||||
}
|
}
|
||||||
self::assertStringContainsString('overflow: auto', $html);
|
self::assertStringContainsString('overflow: auto', $html);
|
||||||
|
self::assertStringContainsString('--background-accent-1-rgb: 99, 179, 255;', $html);
|
||||||
|
self::assertStringContainsString('--background-accent-2-rgb: 168, 85, 247;', $html);
|
||||||
self::assertStringNotContainsString('touch-action: none', $html);
|
self::assertStringNotContainsString('touch-action: none', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ function visualization(initialState) {
|
|||||||
if (!elements.has(id)) {
|
if (!elements.has(id)) {
|
||||||
elements.set(id, {
|
elements.set(id, {
|
||||||
innerHTML: '', textContent: '', value: '', hidden: false,
|
innerHTML: '', textContent: '', value: '', hidden: false,
|
||||||
|
style: {
|
||||||
|
setProperty(name, value) { this[name] = value; }
|
||||||
|
},
|
||||||
addEventListener() {}, classList: { toggle() {} }
|
addEventListener() {}, classList: { toggle() {} }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -30,7 +33,7 @@ function visualization(initialState) {
|
|||||||
window, document,
|
window, document,
|
||||||
getComputedStyle: () => ({ getPropertyValue: () => '' })
|
getComputedStyle: () => ({ getPropertyValue: () => '' })
|
||||||
});
|
});
|
||||||
return { element, window };
|
return { element, window, document };
|
||||||
}
|
}
|
||||||
|
|
||||||
function payload(overrides = {}) {
|
function payload(overrides = {}) {
|
||||||
@@ -86,3 +89,58 @@ test('missing archive values do not hide the attribution limitation', () => {
|
|||||||
assert.equal(ui.element('message').textContent,
|
assert.equal(ui.element('message').textContent,
|
||||||
'No archive data. Different measurement boundaries.');
|
'No archive data. Different measurement boundaries.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('old payloads retain the original Energy Pie background', () => {
|
||||||
|
const ui = visualization(payload());
|
||||||
|
const style = ui.element('energy-app').style;
|
||||||
|
assert.equal(style['--card-color'], '#121216');
|
||||||
|
assert.equal(style['--background-accent-1-rgb'], '99, 179, 255');
|
||||||
|
assert.equal(style['--background-accent-2-rgb'], '168, 85, 247');
|
||||||
|
assert.equal(style['--content-color'], '#f5f5f7');
|
||||||
|
assert.equal(style.colorScheme, 'dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('configured backgrounds apply initially and on visualization updates', () => {
|
||||||
|
const ui = visualization(payload({ colors: {
|
||||||
|
background: '#FFFFFF', backgroundAccent1: '#DCE169', backgroundAccent2: '#4E8181',
|
||||||
|
selfConsumption: '#1259A7', autarky: '#178A55'
|
||||||
|
} }));
|
||||||
|
const style = ui.element('energy-app').style;
|
||||||
|
assert.equal(style['--card-color'], '#FFFFFF');
|
||||||
|
assert.equal(style['--background-accent-1-rgb'], '220, 225, 105');
|
||||||
|
assert.equal(style['--background-accent-2-rgb'], '78, 129, 129');
|
||||||
|
assert.equal(style['--content-color'], '#121216');
|
||||||
|
assert.equal(style.colorScheme, 'light');
|
||||||
|
assert.equal(ui.document.documentElement.style.colorScheme, undefined);
|
||||||
|
|
||||||
|
const state = payload({ colors: {
|
||||||
|
background: '#000000', backgroundAccent1: '#112233', backgroundAccent2: '#445566',
|
||||||
|
selfConsumption: '#1259A7', autarky: '#178A55'
|
||||||
|
} });
|
||||||
|
state.values.Eigenverbrauchsquote = 75;
|
||||||
|
state.values.Autarkiegrad = 50;
|
||||||
|
ui.window.handleMessage(JSON.stringify(state));
|
||||||
|
assert.equal(style['--card-color'], '#000000');
|
||||||
|
assert.equal(style['--background-accent-1-rgb'], '17, 34, 51');
|
||||||
|
assert.equal(style['--background-accent-2-rgb'], '68, 85, 102');
|
||||||
|
assert.equal(style['--content-color'], '#f5f5f7');
|
||||||
|
assert.equal(style.colorScheme, 'dark');
|
||||||
|
assert.match(ui.element('self-consumption-chart').innerHTML, /stroke="#1259A7"/);
|
||||||
|
assert.match(ui.element('autarky-chart').innerHTML, /stroke="#178A55"/);
|
||||||
|
|
||||||
|
ui.window.handleMessage(payload());
|
||||||
|
assert.equal(style['--card-color'], '#121216');
|
||||||
|
assert.equal(style['--background-accent-1-rgb'], '99, 179, 255');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid or incomplete background colors fall back independently', () => {
|
||||||
|
for (const invalid of [null, -1, 16777216, {}, '', '#123', '#123456; color:red', 'url(https://invalid.test/)']) {
|
||||||
|
const ui = visualization(payload({ hasData: false, colors: {
|
||||||
|
background: invalid, backgroundAccent1: '#aAbBcC', backgroundAccent2: invalid
|
||||||
|
} }));
|
||||||
|
const style = ui.element('energy-app').style;
|
||||||
|
assert.equal(style['--card-color'], '#121216');
|
||||||
|
assert.equal(style['--background-accent-1-rgb'], '170, 187, 204');
|
||||||
|
assert.equal(style['--background-accent-2-rgb'], '168, 85, 247');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ return static function (TestContext $test): void {
|
|||||||
$test->assertSame('month', IPS_GetProperty($moduleId, 'Standardzeitraum'));
|
$test->assertSame('month', IPS_GetProperty($moduleId, 'Standardzeitraum'));
|
||||||
$test->assertSame('#1259A7', IPS_GetProperty($moduleId, 'FarbeEigenverbrauch'));
|
$test->assertSame('#1259A7', IPS_GetProperty($moduleId, 'FarbeEigenverbrauch'));
|
||||||
$test->assertSame('#178A55', IPS_GetProperty($moduleId, 'FarbeAutarkie'));
|
$test->assertSame('#178A55', IPS_GetProperty($moduleId, 'FarbeAutarkie'));
|
||||||
|
$test->assertSame('#121216', IPS_GetProperty($moduleId, 'FarbeHintergrund'));
|
||||||
|
$test->assertSame('#63B3FF', IPS_GetProperty($moduleId, 'FarbeHintergrundAkzent1'));
|
||||||
|
$test->assertSame('#A855F7', IPS_GetProperty($moduleId, 'FarbeHintergrundAkzent2'));
|
||||||
|
$test->assertSame(0x1259A7, IPS_GetProperty($moduleId, 'FarbeEigenverbrauchRGB'));
|
||||||
|
$test->assertSame(0x178A55, IPS_GetProperty($moduleId, 'FarbeAutarkieRGB'));
|
||||||
|
$test->assertSame(0x121216, IPS_GetProperty($moduleId, 'FarbeHintergrundRGB'));
|
||||||
|
$test->assertSame(0x63B3FF, IPS_GetProperty($moduleId, 'FarbeHintergrundAkzent1RGB'));
|
||||||
|
$test->assertSame(0xA855F7, IPS_GetProperty($moduleId, 'FarbeHintergrundAkzent2RGB'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$test->runCase('Konfigurationsformular ist gültiges JSON', static function (
|
$test->runCase('Konfigurationsformular ist gültiges JSON', static function (
|
||||||
@@ -58,4 +66,86 @@ return static function (TestContext $test): void {
|
|||||||
IPS_RequestAction($moduleId, 'Refresh', true);
|
IPS_RequestAction($moduleId, 'Refresh', true);
|
||||||
$test->assertSame(102, (int) IPS_GetInstance($moduleId)['InstanceStatus']);
|
$test->assertSame(102, (int) IPS_GetInstance($moduleId)['InstanceStatus']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('Hintergrundfarben akzeptieren Hex- und Symcon-8-Farbwerte', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($moduleId): void {
|
||||||
|
$colors = [
|
||||||
|
'FarbeHintergrund' => '#FFFFFF',
|
||||||
|
'FarbeHintergrundAkzent1' => '0',
|
||||||
|
'FarbeHintergrundAkzent2' => '16777215',
|
||||||
|
];
|
||||||
|
$test->configureInstance($moduleId, $colors);
|
||||||
|
$test->assertInstanceStatus($moduleId);
|
||||||
|
foreach ($colors as $property => $color) {
|
||||||
|
$test->assertSame($color, IPS_GetProperty($moduleId, $property));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Ungueltige Hintergrundfarben werden abgewiesen', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($moduleId): void {
|
||||||
|
foreach (['FarbeHintergrund', 'FarbeHintergrundAkzent1', 'FarbeHintergrundAkzent2'] as $property) {
|
||||||
|
$original = IPS_GetProperty($moduleId, $property);
|
||||||
|
try {
|
||||||
|
$test->configureInstance($moduleId, [$property => 'not-a-color']);
|
||||||
|
$test->assertInstanceStatus($moduleId, 201);
|
||||||
|
} finally {
|
||||||
|
$test->configureInstance($moduleId, [$property => $original]);
|
||||||
|
}
|
||||||
|
$test->assertInstanceStatus($moduleId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Jede Farbauswahl speichert nur die geaenderte Farbe', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($moduleId): void {
|
||||||
|
$properties = ['FarbeEigenverbrauch', 'FarbeAutarkie', 'FarbeHintergrund',
|
||||||
|
'FarbeHintergrundAkzent1', 'FarbeHintergrundAkzent2'];
|
||||||
|
foreach ($properties as $index => $property) {
|
||||||
|
$before = json_decode(IPS_GetConfiguration($moduleId), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$rgb = [0xABCDEF, 0x000001, 0, 0xFFFFFF, 0x12AB34][$index];
|
||||||
|
$test->configureInstance($moduleId, [$property . 'RGB' => $rgb]);
|
||||||
|
$test->assertInstanceStatus($moduleId);
|
||||||
|
foreach ($properties as $other) {
|
||||||
|
$test->assertSame($other === $property ? $rgb : $before[$other . 'RGB'],
|
||||||
|
IPS_GetProperty($moduleId, $other . 'RGB'));
|
||||||
|
$test->assertSame($other === $property ? sprintf('#%06X', $rgb) : $before[$other],
|
||||||
|
IPS_GetProperty($moduleId, $other));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$before = json_decode(IPS_GetConfiguration($moduleId), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
$test->configureInstance($moduleId, ['Aktualisierungsintervall' => 120]);
|
||||||
|
IPS_ApplyChanges($moduleId);
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$test->assertSame($before[$property], IPS_GetProperty($moduleId, $property));
|
||||||
|
$test->assertSame($before[$property . 'RGB'], IPS_GetProperty($moduleId, $property . 'RGB'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Alte Farbschnittstelle bleibt mit der Farbauswahl synchron', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($moduleId): void {
|
||||||
|
foreach (['#000001' => 1, '11259375' => 0xABCDEF, '0' => 0] as $legacy => $rgb) {
|
||||||
|
$test->configureInstance($moduleId, ['FarbeHintergrund' => (string) $legacy]);
|
||||||
|
$test->assertSame($rgb, IPS_GetProperty($moduleId, 'FarbeHintergrundRGB'));
|
||||||
|
$test->assertSame((string) $legacy, IPS_GetProperty($moduleId, 'FarbeHintergrund'));
|
||||||
|
}
|
||||||
|
$test->configureInstance($moduleId, [
|
||||||
|
'FarbeHintergrund' => '#123456', 'FarbeHintergrundRGB' => 0x987654,
|
||||||
|
]);
|
||||||
|
$test->assertSame('#987654', IPS_GetProperty($moduleId, 'FarbeHintergrund'));
|
||||||
|
$test->assertSame(0x987654, IPS_GetProperty($moduleId, 'FarbeHintergrundRGB'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Ungueltige numerische Farbauswahl wird abgewiesen', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($moduleId): void {
|
||||||
|
foreach ([-1, 0x1000000] as $invalid) {
|
||||||
|
$test->configureInstance($moduleId, ['FarbeHintergrundRGB' => $invalid]);
|
||||||
|
$test->assertInstanceStatus($moduleId, 201);
|
||||||
|
}
|
||||||
|
$test->configureInstance($moduleId, ['FarbeHintergrundRGB' => 0x121216]);
|
||||||
|
$test->assertInstanceStatus($moduleId);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user