diff --git a/Energiediagramm/form.json b/Energiediagramm/form.json index 4b56da5..4e69d11 100644 --- a/Energiediagramm/form.json +++ b/Energiediagramm/form.json @@ -66,18 +66,42 @@ "items": [ { "type": "SelectColor", - "name": "FarbeEigenverbrauch", + "name": "FarbeEigenverbrauchRGB", "caption": "Eigenverbrauchsquote", "allowTransparent": false }, { "type": "SelectColor", - "name": "FarbeAutarkie", + "name": "FarbeAutarkieRGB", "caption": "Autarkiegrad", "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", "name": "Debug", diff --git a/Energiediagramm/module.html b/Energiediagramm/module.html index 204eefa..9b3624d 100644 --- a/Energiediagramm/module.html +++ b/Energiediagramm/module.html @@ -73,6 +73,8 @@ #energy-app { --card-color: #121216; --content-color: #f5f5f7; + --background-accent-1-rgb: 99, 179, 255; + --background-accent-2-rgb: 168, 85, 247; position: relative; isolation: isolate; color-scheme: dark; @@ -86,16 +88,16 @@ 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 { 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%); + 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(var(--background-accent-2-rgb), 0.38), transparent 62%), + radial-gradient(620px 520px at 55% 85%, rgba(var(--background-accent-1-rgb), 0.26), transparent 64%); filter: blur(18px); pointer-events: none; } @@ -309,21 +311,29 @@ const selfConsumptionChart = document.getElementById('self-consumption-chart'); const autarkyChart = document.getElementById('autarky-chart'); - function syncColorScheme() { - const cardColor = getComputedStyle(document.documentElement) - .getPropertyValue('--card-color') - .trim(); - const match = cardColor.match(/^#?([0-9a-f]{6})$/i); - if (!match) { - return; - } + function colorRgb(color) { + const value = Number.parseInt(color.slice(1), 16); + return [(value >> 16) & 255, (value >> 8) & 255, value & 255]; + } - const value = Number.parseInt(match[1], 16); - const red = (value >> 16) & 255; - const green = (value >> 8) & 255; - const blue = value & 255; - const luminance = ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)) / 255; - document.documentElement.style.colorScheme = luminance < 0.5 ? 'dark' : 'light'; + function syncBackground(colors) { + const safeColor = (value, fallback) => typeof value === 'string' + && /^#[0-9a-f]{6}$/i.test(value) ? value : fallback; + const background = safeColor(colors.background, '#121216'); + const accent1 = safeColor(colors.backgroundAccent1, '#63B3FF'); + 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) { @@ -425,7 +435,8 @@ return; } - syncColorScheme(); + const colors = data.colors || {}; + syncBackground(colors); range.value = data.range || 'day'; date.value = data.date || ''; customStart.value = data.customStart || ''; @@ -447,7 +458,6 @@ metric('Einspeisung', values.Einspeisung) ].join(''); - const colors = data.colors || {}; donut( selfConsumptionChart, data.sharesAvailable === false ? null : values.Eigenverbrauchsquote, diff --git a/Energiediagramm/module.php b/Energiediagramm/module.php index 6918d9c..5d3d47b 100644 --- a/Energiediagramm/module.php +++ b/Energiediagramm/module.php @@ -15,6 +15,16 @@ class Energiediagramm extends IPSModule private const ATTR_CUSTOM_END = 'CustomEnd'; private const COLOR_SELF_CONSUMPTION = '#2F80ED'; 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 { @@ -30,6 +40,13 @@ class Energiediagramm extends IPSModule $this->RegisterPropertyInteger('Aktualisierungsintervall', 60); $this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION); $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); $today = date('Y-m-d'); @@ -37,6 +54,7 @@ class Energiediagramm extends IPSModule $this->RegisterAttributeString(self::ATTR_DATE, $today); $this->RegisterAttributeString(self::ATTR_CUSTOM_START, $today); $this->RegisterAttributeString(self::ATTR_CUSTOM_END, $today); + $this->RegisterAttributeString('ColorConfiguration', '{}'); $visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=') ? 2 @@ -53,6 +71,10 @@ class Energiediagramm extends IPSModule { parent::ApplyChanges(); + if ($this->synchronizeColorProperties()) { + return; + } + $today = date('Y-m-d'); $defaultRange = $this->ReadPropertyString('Standardzeitraum'); if (!in_array($defaultRange, ['day', 'week', 'month', 'year', 'total'], true)) { @@ -84,6 +106,51 @@ class Energiediagramm extends IPSModule $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 { $path = __DIR__ . '/module.html'; @@ -277,6 +344,18 @@ class Energiediagramm extends IPSModule $this->ReadPropertyString('FarbeAutarkie'), 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), ]; @@ -435,6 +514,14 @@ class Energiediagramm extends IPSModule 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 && $this->ReadPropertyInteger('Aktualisierungsintervall') >= 10 && $this->ReadPropertyInteger('Aktualisierungsintervall') <= 3600 @@ -442,15 +529,7 @@ class Energiediagramm extends IPSModule $this->ReadPropertyString('Standardzeitraum'), ['day', 'week', 'month', 'year', 'total'], true - ) - && EnergyChartCalculator::normalizeColor( - $this->ReadPropertyString('FarbeEigenverbrauch'), - '' - ) !== '' - && EnergyChartCalculator::normalizeColor( - $this->ReadPropertyString('FarbeAutarkie'), - '' - ) !== ''; + ); } private function requiredSourceIdsAreEmpty(): bool diff --git a/docs/ENERGY_PIE_RELEASE_20261007.md b/docs/ENERGY_PIE_RELEASE_20261007.md new file mode 100644 index 0000000..e1dcb66 --- /dev/null +++ b/docs/ENERGY_PIE_RELEASE_20261007.md @@ -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. diff --git a/tests/EnergiediagrammModulstrukturTest.php b/tests/EnergiediagrammModulstrukturTest.php index c4e8690..9c15fd4 100644 --- a/tests/EnergiediagrammModulstrukturTest.php +++ b/tests/EnergiediagrammModulstrukturTest.php @@ -37,10 +37,29 @@ final class EnergiediagrammModulstrukturTest extends TestCase 'Standardzeitraum', 'FarbeEigenverbrauch', 'FarbeAutarkie', + 'FarbeHintergrund', + 'FarbeHintergrundAkzent1', + 'FarbeHintergrundAkzent2', ] as $property) { 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 @@ -54,7 +73,7 @@ final class EnergiediagrammModulstrukturTest extends TestCase self::assertStringContainsString('padding: 58px 10px 10px', $html); self::assertStringContainsString('background-color: var(--card-color, transparent)', $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::assertStringNotContainsString("addEventListener('load'", $html); } @@ -71,15 +90,17 @@ final class EnergiediagrammModulstrukturTest extends TestCase 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%)', + '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(var(--background-accent-2-rgb), 0.38), transparent 62%)', + 'radial-gradient(620px 520px at 55% 85%, rgba(var(--background-accent-1-rgb), 0.26), transparent 64%)', 'filter: blur(18px)', 'pointer-events: none', ] as $style) { self::assertStringContainsString($style, $background[1]); } 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); } diff --git a/tests/EnergyPieVisualizationTest.cjs b/tests/EnergyPieVisualizationTest.cjs index 3ab9a5a..67195ed 100644 --- a/tests/EnergyPieVisualizationTest.cjs +++ b/tests/EnergyPieVisualizationTest.cjs @@ -12,6 +12,9 @@ function visualization(initialState) { if (!elements.has(id)) { elements.set(id, { innerHTML: '', textContent: '', value: '', hidden: false, + style: { + setProperty(name, value) { this[name] = value; } + }, addEventListener() {}, classList: { toggle() {} } }); } @@ -30,7 +33,7 @@ function visualization(initialState) { window, document, getComputedStyle: () => ({ getPropertyValue: () => '' }) }); - return { element, window }; + return { element, window, document }; } function payload(overrides = {}) { @@ -86,3 +89,58 @@ test('missing archive values do not hide the attribution limitation', () => { assert.equal(ui.element('message').textContent, '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'); + } +}); diff --git a/tests/Symcon/modules/Energiediagramm.php b/tests/Symcon/modules/Energiediagramm.php index a3f89cc..2eab721 100644 --- a/tests/Symcon/modules/Energiediagramm.php +++ b/tests/Symcon/modules/Energiediagramm.php @@ -29,6 +29,14 @@ return static function (TestContext $test): void { $test->assertSame('month', IPS_GetProperty($moduleId, 'Standardzeitraum')); $test->assertSame('#1259A7', IPS_GetProperty($moduleId, 'FarbeEigenverbrauch')); $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 ( @@ -58,4 +66,86 @@ return static function (TestContext $test): void { IPS_RequestAction($moduleId, 'Refresh', true); $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); + }); };