feat(energy-pie): finish configurable colors and Symcon 8 migration
Tests / test (push) Waiting to run

This commit is contained in:
dh
2026-10-07 17:16:00 +00:00
parent 5b6125d086
commit 2ce95e77f8
7 changed files with 376 additions and 37 deletions
+26 -2
View File
@@ -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",
+30 -20
View File
@@ -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,
+88 -9
View File
@@ -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
+57
View File
@@ -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.
+26 -5
View File
@@ -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);
} }
+59 -1
View File
@@ -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');
}
});
+90
View File
@@ -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);
});
}; };