Compare commits
6
Commits
f11b1fbe61
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b6125d086 | ||
|
|
c5f924bd19 | ||
|
|
82860c0eb9 | ||
|
|
59eec738ab | ||
|
|
40c6a17dd1 | ||
|
|
1f945be8f1 |
@@ -21,6 +21,14 @@ jobs:
|
|||||||
- name: Abhaengigkeiten installieren
|
- name: Abhaengigkeiten installieren
|
||||||
run: composer install --no-interaction --no-progress --prefer-dist
|
run: composer install --no-interaction --no-progress --prefer-dist
|
||||||
|
|
||||||
|
- name: Composer-Metadaten pruefen
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Abhaengigkeiten auf Sicherheitsmeldungen pruefen
|
||||||
|
run: |
|
||||||
|
composer audit --no-interaction
|
||||||
|
composer audit --working-dir=Verbrauchskostenreport/libs --locked --no-interaction
|
||||||
|
|
||||||
- name: Unit-Tests und Syntaxpruefung ausfuehren
|
- name: Unit-Tests und Syntaxpruefung ausfuehren
|
||||||
run: composer check
|
run: composer check
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Alle wesentlichen Aenderungen an Enelix Utils werden in dieser Datei dokumentiert.
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Energiediagramm zeigt wieder den urspruenglichen dunklen Energy-Pie-Hintergrund
|
||||||
|
mit blauen und violetten Farbverlaeufen, auch bei hellem Visualisierungsthema.
|
||||||
|
Bedienelemente bleiben lesbar; Konfiguration und Berechnungen sind unveraendert.
|
||||||
|
|
||||||
|
## 0.1 Build 1 - Erste Beta - 2026-09-29
|
||||||
|
|
||||||
|
### Enthalten
|
||||||
|
|
||||||
|
- Verbrauchskostenreport mit PDF-Ausgabe und Schweizer QR-Zahlteil nach SIX 2.3.
|
||||||
|
- Verbrauchskostenreport mit Grundlizenz sowie Strom- und Nebenzaehlerkontingenten.
|
||||||
|
- Virtuelle Batterie und VGT-Schnittstelle.
|
||||||
|
- CC100 Hardware und generisches Shelly Modul.
|
||||||
|
- Energiediagramm fuer die IP-Symcon-Kachelvisualisierung.
|
||||||
|
|
||||||
|
### Beta-Aenderungen
|
||||||
|
|
||||||
|
- Energiediagramm uebernimmt Kachel- und Textfarbe der Visualisierung; Zeitraum-
|
||||||
|
und Datumsauswahl bleiben in hellen und dunklen Themen lesbar.
|
||||||
|
- Dezimale IP-Symcon-Farbwerte werden im Energiediagramm korrekt verarbeitet.
|
||||||
|
- Kachelabstand fuer den Symcon-Kopf und versionsabhaengiger Vollbildmodus.
|
||||||
|
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
|
||||||
|
- Composer-Metadaten- und Abhaengigkeitspruefung in der CI.
|
||||||
|
|
||||||
|
### Hinweise
|
||||||
|
|
||||||
|
- Diese Version ist fuer kontrollierte Anlagen- und Feldtests bestimmt.
|
||||||
|
- Vor produktivem Einsatz sind Sicherung und anlagenspezifische Abnahme erforderlich.
|
||||||
|
- Korrekturen werden vorwaerts ueber `develop` und anschliessend `beta` verteilt.
|
||||||
@@ -24,14 +24,28 @@ zusaetzlich ein frei waehlbarer inklusiver Datumsbereich und die Navigation
|
|||||||
durch abgeschlossene Zeitraeume zur Verfuegung.
|
durch abgeschlossene Zeitraeume zur Verfuegung.
|
||||||
|
|
||||||
Die Farben fuer Eigenverbrauchsquote und Autarkiegrad werden mit den beiden
|
Die Farben fuer Eigenverbrauchsquote und Autarkiegrad werden mit den beiden
|
||||||
Farbauswahlen in der Instanzkonfiguration festgelegt.
|
Farbauswahlen in der Instanzkonfiguration festgelegt. Das Modul verarbeitet
|
||||||
|
sowohl aktuelle Hex-Farbwerte als auch die von IP-Symcon 8.0 gespeicherten
|
||||||
|
dezimalen Farbwerte.
|
||||||
|
|
||||||
## Darstellung
|
## Darstellung
|
||||||
|
|
||||||
Die Kachel und ihre vergroesserte Ansicht erhalten beim Aufbau sofort einen
|
Der Inhalt beginnt unterhalb des von Symcon eingeblendeten Kachelkopfs, damit
|
||||||
vollstaendigen Datenstand. Beim Oeffnen und beim erneuten Sichtbarwerden wird
|
Zeitraum und Datum vollstaendig bedienbar bleiben. Der urspruengliche Energy-Pie-
|
||||||
die Darstellung aktualisiert. Das Diagrammraster wechselt bei geringer Breite
|
Hintergrund mit dunkler Grundfarbe und blauen/violetten Farbverlaeufen bleibt
|
||||||
automatisch von zwei Spalten auf eine Spalte.
|
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.
|
||||||
|
|
||||||
|
IP-Symcon 8.x unterstuetzt HTML-SDK-Darstellungen nur in der normalen Kachel.
|
||||||
|
Der vom Client angebotene Vergroesserungsdialog kann deshalb leer bleiben.
|
||||||
|
Ab IP-Symcon 9.0 aktiviert das Modul automatisch den zusaetzlichen
|
||||||
|
HTML-SDK-Vollbildmodus.
|
||||||
|
|
||||||
## Berechnung
|
## Berechnung
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -32,6 +32,15 @@
|
|||||||
"maximum": 1000000,
|
"maximum": 1000000,
|
||||||
"digits": 6
|
"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",
|
"type": "Select",
|
||||||
"name": "Standardzeitraum",
|
"name": "Standardzeitraum",
|
||||||
|
|||||||
@@ -82,16 +82,17 @@ final class EnergyChartCalculator
|
|||||||
* Einspeisung: float,
|
* Einspeisung: float,
|
||||||
* Netzbezug: float,
|
* Netzbezug: float,
|
||||||
* Hausverbrauch: float,
|
* Hausverbrauch: float,
|
||||||
* Eigenverbrauch: float,
|
* Eigenverbrauch: float|null,
|
||||||
* Eigenverbrauchsquote: float,
|
* Eigenverbrauchsquote: float|null,
|
||||||
* Autarkiegrad: float
|
* Autarkiegrad: float|null
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public static function metrics(
|
public static function metrics(
|
||||||
float $production,
|
float $production,
|
||||||
float $feedIn,
|
float $feedIn,
|
||||||
float $grid,
|
float $grid,
|
||||||
?float $consumption = null
|
?float $consumption = null,
|
||||||
|
bool $sharesAvailable = true
|
||||||
): array {
|
): array {
|
||||||
$production = max(0.0, $production);
|
$production = max(0.0, $production);
|
||||||
$feedIn = max(0.0, $feedIn);
|
$feedIn = max(0.0, $feedIn);
|
||||||
@@ -106,21 +107,31 @@ final class EnergyChartCalculator
|
|||||||
'Einspeisung' => $feedIn,
|
'Einspeisung' => $feedIn,
|
||||||
'Netzbezug' => $grid,
|
'Netzbezug' => $grid,
|
||||||
'Hausverbrauch' => $house,
|
'Hausverbrauch' => $house,
|
||||||
'Eigenverbrauch' => $selfConsumption,
|
'Eigenverbrauch' => $sharesAvailable ? $selfConsumption : null,
|
||||||
'Eigenverbrauchsquote' => $production > 0.0
|
'Eigenverbrauchsquote' => !$sharesAvailable ? null : ($production > 0.0
|
||||||
? min(100.0, ($selfConsumption / $production) * 100.0)
|
? min(100.0, ($selfConsumption / $production) * 100.0)
|
||||||
: 0.0,
|
: 0.0),
|
||||||
'Autarkiegrad' => $house > 0.0
|
'Autarkiegrad' => !$sharesAvailable ? null : ($house > 0.0
|
||||||
? min(100.0, ($selfConsumption / $house) * 100.0)
|
? min(100.0, ($selfConsumption / $house) * 100.0)
|
||||||
: 0.0,
|
: 0.0),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeColor(string $color, string $fallback): string
|
public static function normalizeColor(string $color, string $fallback): string
|
||||||
{
|
{
|
||||||
return preg_match('/^#[0-9A-Fa-f]{6}$/', $color) === 1
|
$color = trim($color);
|
||||||
? strtoupper($color)
|
if (preg_match('/^#[0-9A-Fa-f]{6}$/', $color) === 1) {
|
||||||
: strtoupper($fallback);
|
return strtoupper($color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^[0-9]+$/', $color) === 1) {
|
||||||
|
$legacyColor = (int) $color;
|
||||||
|
if ($legacyColor >= 0 && $legacyColor <= 0xFFFFFF) {
|
||||||
|
return sprintf('#%06X', $legacyColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtoupper($fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function date(string $date): DateTimeImmutable
|
private static function date(string $date): DateTimeImmutable
|
||||||
|
|||||||
+76
-17
@@ -63,19 +63,41 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: var(--content-color, inherit);
|
||||||
|
background-color: var(--card-color, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#energy-app {
|
#energy-app {
|
||||||
|
--card-color: #121216;
|
||||||
|
--content-color: #f5f5f7;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
color-scheme: dark;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 14px;
|
padding: 58px 14px 14px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
color: inherit;
|
color: var(--content-color, inherit);
|
||||||
background: transparent;
|
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,
|
.toolbar,
|
||||||
@@ -104,11 +126,16 @@
|
|||||||
border: 1px solid rgba(127, 127, 127, 0.45);
|
border: 1px solid rgba(127, 127, 127, 0.45);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
color: inherit;
|
color: var(--content-color, inherit);
|
||||||
background: rgba(127, 127, 127, 0.08);
|
background-color: var(--card-color, transparent);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
color: var(--content-color, CanvasText);
|
||||||
|
background-color: var(--card-color, Canvas);
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
@@ -221,6 +248,11 @@
|
|||||||
dominant-baseline: middle;
|
dominant-baseline: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.donut-label.unavailable {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -234,7 +266,7 @@
|
|||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
#energy-app {
|
#energy-app {
|
||||||
padding: 10px;
|
padding: 58px 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar,
|
.toolbar,
|
||||||
@@ -277,6 +309,23 @@
|
|||||||
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() {
|
||||||
|
const cardColor = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--card-color')
|
||||||
|
.trim();
|
||||||
|
const match = cardColor.match(/^#?([0-9a-f]{6})$/i);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 send(action, value) {
|
function send(action, value) {
|
||||||
if (typeof requestAction !== 'function') {
|
if (typeof requestAction !== 'function') {
|
||||||
return;
|
return;
|
||||||
@@ -342,6 +391,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function donut(target, percent, color) {
|
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 value = Math.max(0, Math.min(100, asNumber(percent)));
|
||||||
const radius = 58;
|
const radius = 58;
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
@@ -349,15 +400,17 @@
|
|||||||
|
|
||||||
target.innerHTML = [
|
target.innerHTML = [
|
||||||
'<svg viewBox="0 0 176 176" role="img" aria-label="'
|
'<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
|
'<circle class="donut-track" cx="88" cy="88" r="' + radius
|
||||||
+ '" fill="none" stroke-width="18"></circle>',
|
+ '" 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"'
|
+ '" fill="none" stroke="' + color + '" stroke-width="18"'
|
||||||
+ ' stroke-linecap="round" transform="rotate(-90 88 88)"'
|
+ ' stroke-linecap="round" transform="rotate(-90 88 88)"'
|
||||||
+ ' stroke-dasharray="' + filled + ' ' + circumference + '"></circle>',
|
+ ' stroke-dasharray="' + filled + ' ' + circumference + '"></circle>' : '',
|
||||||
'<text class="donut-label" x="88" y="91">'
|
available ? '<text class="donut-label" x="88" y="91">'
|
||||||
+ value.toFixed(1) + '%</text>',
|
+ 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>'
|
'</svg>'
|
||||||
].join('');
|
].join('');
|
||||||
}
|
}
|
||||||
@@ -372,6 +425,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncColorScheme();
|
||||||
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 || '';
|
||||||
@@ -396,19 +450,24 @@
|
|||||||
const colors = data.colors || {};
|
const colors = data.colors || {};
|
||||||
donut(
|
donut(
|
||||||
selfConsumptionChart,
|
selfConsumptionChart,
|
||||||
values.Eigenverbrauchsquote,
|
data.sharesAvailable === false ? null : values.Eigenverbrauchsquote,
|
||||||
colors.selfConsumption || '#2F80ED'
|
colors.selfConsumption || '#2F80ED'
|
||||||
);
|
);
|
||||||
donut(
|
donut(
|
||||||
autarkyChart,
|
autarkyChart,
|
||||||
values.Autarkiegrad,
|
data.sharesAvailable === false ? null : values.Autarkiegrad,
|
||||||
colors.autarky || '#22A06B'
|
colors.autarky || '#22A06B'
|
||||||
);
|
);
|
||||||
|
|
||||||
message.hidden = data.hasData !== false;
|
const hints = [];
|
||||||
message.textContent = data.hasData === false
|
if (data.hasData === false) {
|
||||||
? (data.noDataHint || 'Keine Archivwerte verfügbar.')
|
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) {
|
window.handleMessage = function (data) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Energiediagramm extends IPSModule
|
|||||||
$this->RegisterPropertyInteger('NetzbezugZaehlerID', 0);
|
$this->RegisterPropertyInteger('NetzbezugZaehlerID', 0);
|
||||||
$this->RegisterPropertyInteger('VerbrauchZaehlerID', 0);
|
$this->RegisterPropertyInteger('VerbrauchZaehlerID', 0);
|
||||||
$this->RegisterPropertyFloat('Zaehlerfaktor', 1.0);
|
$this->RegisterPropertyFloat('Zaehlerfaktor', 1.0);
|
||||||
|
$this->RegisterPropertyBoolean('EnergieanteileBerechenbar', true);
|
||||||
$this->RegisterPropertyString('Standardzeitraum', 'day');
|
$this->RegisterPropertyString('Standardzeitraum', 'day');
|
||||||
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
|
$this->RegisterPropertyInteger('Aktualisierungsintervall', 60);
|
||||||
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
|
$this->RegisterPropertyString('FarbeEigenverbrauch', self::COLOR_SELF_CONSUMPTION);
|
||||||
@@ -37,7 +38,10 @@ class Energiediagramm extends IPSModule
|
|||||||
$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->SetVisualizationType(1);
|
$visualizationType = version_compare(IPS_GetKernelVersion(), '9.0', '>=')
|
||||||
|
? 2
|
||||||
|
: 1;
|
||||||
|
$this->SetVisualizationType($visualizationType);
|
||||||
$this->RegisterTimer(
|
$this->RegisterTimer(
|
||||||
'Refresh',
|
'Refresh',
|
||||||
0,
|
0,
|
||||||
@@ -250,6 +254,7 @@ class Energiediagramm extends IPSModule
|
|||||||
$customStart,
|
$customStart,
|
||||||
$customEnd
|
$customEnd
|
||||||
);
|
);
|
||||||
|
$sharesAvailable = $this->ReadPropertyBoolean('EnergieanteileBerechenbar');
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'range' => $range,
|
'range' => $range,
|
||||||
@@ -260,6 +265,9 @@ class Energiediagramm extends IPSModule
|
|||||||
'tEnd' => $end,
|
'tEnd' => $end,
|
||||||
'hasData' => false,
|
'hasData' => false,
|
||||||
'noDataHint' => 'Energiezähler konfigurieren.',
|
'noDataHint' => 'Energiezähler konfigurieren.',
|
||||||
|
'sharesAvailable' => $sharesAvailable,
|
||||||
|
'attributionHint' => $sharesAvailable ? ''
|
||||||
|
: 'Eigenverbrauch und Autarkie sind bei unterschiedlichen Messgrenzen nicht eindeutig bestimmbar. Die Energiesummen bleiben separat sichtbar.',
|
||||||
'colors' => [
|
'colors' => [
|
||||||
'selfConsumption' => EnergyChartCalculator::normalizeColor(
|
'selfConsumption' => EnergyChartCalculator::normalizeColor(
|
||||||
$this->ReadPropertyString('FarbeEigenverbrauch'),
|
$this->ReadPropertyString('FarbeEigenverbrauch'),
|
||||||
@@ -270,7 +278,7 @@ class Energiediagramm extends IPSModule
|
|||||||
self::COLOR_AUTARKY
|
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()) {
|
if (!$this->configurationIsValid()) {
|
||||||
@@ -326,7 +334,8 @@ class Energiediagramm extends IPSModule
|
|||||||
$production['value'] * $factor,
|
$production['value'] * $factor,
|
||||||
$feedIn['value'] * $factor,
|
$feedIn['value'] * $factor,
|
||||||
$grid['value'] * $factor,
|
$grid['value'] * $factor,
|
||||||
$consumption
|
$consumption,
|
||||||
|
$sharesAvailable
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$payload['hasData']) {
|
if (!$payload['hasData']) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ Unabhaengige Zusatzmodule fuer IP-Symcon. Dieses Repository ist nicht vom Enelix
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Das Repository befindet sich im Aufbau. Verbrauchskostenreport, Virtuelle
|
Version 0.1 Build 1 ist die erste Beta fuer kontrollierte Anlagen- und
|
||||||
Batterie, CC100 Hardware, Energiediagramm, VGT-Schnittstelle und Shelly Modul
|
Feldtests. Verbrauchskostenreport, Virtuelle Batterie, CC100 Hardware,
|
||||||
sind als installierbare IP-Symcon-Module enthalten.
|
Energiediagramm, VGT-Schnittstelle und Shelly Modul sind als installierbare
|
||||||
|
IP-Symcon-Module enthalten. Produktiver Einsatz setzt eine anlagenspezifische
|
||||||
|
Pruefung und Sicherung voraus.
|
||||||
|
|
||||||
## Module
|
## Module
|
||||||
|
|
||||||
@@ -23,6 +25,10 @@ Punkten stehen in der [Modulübersicht](docs/module/README.md).
|
|||||||
Prognose, Lizenzierung und Störüberwachung sind keine Module dieses
|
Prognose, Lizenzierung und Störüberwachung sind keine Module dieses
|
||||||
Repositories. Die Utils-Module bleiben auch untereinander unabhängig.
|
Repositories. Die Utils-Module bleiben auch untereinander unabhängig.
|
||||||
|
|
||||||
|
## Release-Hinweise
|
||||||
|
|
||||||
|
Die Aenderungen der ersten Beta stehen im [Changelog](CHANGELOG.md).
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- IP-Symcon ab Version 8.0
|
- IP-Symcon ab Version 8.0
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ Abmelden des WebHooks beim Löschen einer Instanz.
|
|||||||
- erzeugt optional einen Schweizer QR-Zahlteil
|
- erzeugt optional einen Schweizer QR-Zahlteil
|
||||||
- unterstützt den konfigurierbaren MWST-Ausweis
|
- unterstützt den konfigurierbaren MWST-Ausweis
|
||||||
|
|
||||||
|
## Lizenzierung
|
||||||
|
|
||||||
|
Der Report benoetigt eine Grundlizenz. Zusaetzlich prueft das Modul getrennte
|
||||||
|
Kontingente fuer konfigurierte Stromzaehler und Nebenzaehler. Lizenzcodes werden
|
||||||
|
zur Laufzeit gegen `https://license.enelix.ch` geprueft; Zugangsdaten oder
|
||||||
|
Lizenzcodes werden nicht im Repository gespeichert.
|
||||||
|
|
||||||
## QR-Zahlteil
|
## QR-Zahlteil
|
||||||
|
|
||||||
Der QR-Zahlteil verwendet strukturierte Adressen (`S`) nach SIX SPS 2.3.
|
Der QR-Zahlteil verwendet strukturierte Adressen (`S`) nach SIX SPS 2.3.
|
||||||
@@ -30,6 +37,7 @@ Benutzer- und Zahlungsempfängeradresse benötigen Name, PLZ, Ort und ISO-2-Land
|
|||||||
|
|
||||||
| Property | Beschreibung |
|
| Property | Beschreibung |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `Lizenzcode` | Laufzeitparameter fuer die Freigabe am Lizenzdienst |
|
||||||
| `Users` | Benutzerliste |
|
| `Users` | Benutzerliste |
|
||||||
| `PowerMeters` | Stromzählerliste |
|
| `PowerMeters` | Stromzählerliste |
|
||||||
| `WaterMeters` | Nebenkostenzähler |
|
| `WaterMeters` | Nebenkostenzähler |
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
{
|
{
|
||||||
"elements": [
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Lizenzierung",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Lizenzcode",
|
||||||
|
"caption": "Verbrauchskostenlizenzcode",
|
||||||
|
"placeholder": "ENX-XXXX-XXXX-XXXX-XXXX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"name": "LizenzInstallationID",
|
||||||
|
"caption": "Installations-ID wird erzeugt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"name": "LizenzInformation",
|
||||||
|
"caption": "Verbrauchskostenlizenzcode fehlt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "LizenzPruefen",
|
||||||
|
"caption": "Lizenz prüfen und binden",
|
||||||
|
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "LizenzVerwalten",
|
||||||
|
"caption": "Lizenz verwalten",
|
||||||
|
"link": true,
|
||||||
|
"onClick": "echo 'https://license.enelix.ch';"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "Label",
|
"type": "Label",
|
||||||
"caption": "🧾 Einstellungen Verbrauchskostenreport"
|
"caption": "🧾 Einstellungen Verbrauchskostenreport"
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ class VerbrauchskostenreportPDF extends TCPDF
|
|||||||
|
|
||||||
class Verbrauchskostenreport extends IPSModule
|
class Verbrauchskostenreport extends IPSModule
|
||||||
{
|
{
|
||||||
|
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
||||||
|
private const LIZENZ_RETRY_SEKUNDEN = 900;
|
||||||
|
private const KATALOG_GRUNDLIZENZ = 'cost_report';
|
||||||
|
private const KATALOG_STROMZAEHLER = 'cost_meter_power';
|
||||||
|
private const KATALOG_NEBENZAEHLER = 'cost_meter_aux';
|
||||||
|
|
||||||
/** @var array<int, array<string, array<string, float|string>>> */
|
/** @var array<int, array<string, array<string, float|string>>> */
|
||||||
private array $powerCostCache = [];
|
private array $powerCostCache = [];
|
||||||
|
|
||||||
@@ -47,6 +53,7 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->RegisterPropertyString('PowerMeters', '[]');
|
$this->RegisterPropertyString('PowerMeters', '[]');
|
||||||
$this->RegisterPropertyString('WaterMeters', '[]');
|
$this->RegisterPropertyString('WaterMeters', '[]');
|
||||||
$this->RegisterPropertyString('Tariffs', '[]');
|
$this->RegisterPropertyString('Tariffs', '[]');
|
||||||
|
$this->RegisterPropertyString('Lizenzcode', '');
|
||||||
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
||||||
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
||||||
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
||||||
@@ -64,6 +71,15 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->RegisterPropertyString('CreditorCountry', 'CH');
|
$this->RegisterPropertyString('CreditorCountry', 'CH');
|
||||||
$this->RegisterPropertyString('BankIBAN', '');
|
$this->RegisterPropertyString('BankIBAN', '');
|
||||||
|
|
||||||
|
$this->RegisterAttributeString('LizenzInstallationID', '');
|
||||||
|
$this->RegisterAttributeString('LizenzLease', '{}');
|
||||||
|
$this->RegisterAttributeString('LizenzcodeHash', '');
|
||||||
|
$this->RegisterAttributeString('LizenzLetzterFehler', '');
|
||||||
|
$this->RegisterAttributeInteger('LizenzNaechsterVersuch', 0);
|
||||||
|
if ($this->ReadAttributeString('LizenzInstallationID') === '') {
|
||||||
|
$this->WriteAttributeString('LizenzInstallationID', $this->ErzeugeUuidV4());
|
||||||
|
}
|
||||||
|
|
||||||
$this->RegisterTariffVariableProfiles();
|
$this->RegisterTariffVariableProfiles();
|
||||||
|
|
||||||
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
|
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
|
||||||
@@ -106,6 +122,8 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->InitializeVisuTariffDefaults();
|
$this->InitializeVisuTariffDefaults();
|
||||||
$this->UpdateTariffOverview();
|
$this->UpdateTariffOverview();
|
||||||
$this->UpdateUserOverview();
|
$this->UpdateUserOverview();
|
||||||
|
$this->AktualisiereLizenz($this->ReadPropertyString('Lizenzcode'), false);
|
||||||
|
$this->AktualisiereLizenzForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function Destroy()
|
public function Destroy()
|
||||||
@@ -630,6 +648,278 @@ form.onsubmit=function(){
|
|||||||
return $this->SaveTariffTableFromPayload(json_encode($rows));
|
return $this->SaveTariffTableFromPayload(json_encode($rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function AktualisiereLizenz(string $lizenzcode, bool $erzwingen): bool
|
||||||
|
{
|
||||||
|
$code = $this->NormalisiereLizenzcode($lizenzcode);
|
||||||
|
if (!$this->IstLizenzcodeGueltig($code)) {
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLetzterFehler',
|
||||||
|
$code === '' ? 'Verbrauchskostenlizenzcode fehlt.' : 'Verbrauchskostenlizenzcode ist ungültig.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$installationID = $this->ReadAttributeString('LizenzInstallationID');
|
||||||
|
if ($installationID === '') {
|
||||||
|
$installationID = $this->ErzeugeUuidV4();
|
||||||
|
$this->WriteAttributeString('LizenzInstallationID', $installationID);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeHash = hash('sha256', $code);
|
||||||
|
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
|
||||||
|
$lease = $this->LeseLizenzLease();
|
||||||
|
$lokalGueltig = false;
|
||||||
|
if ($gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash)) {
|
||||||
|
try {
|
||||||
|
$this->PruefeLizenzLease($lease, $installationID);
|
||||||
|
$lokalGueltig = true;
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$lokalGueltig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$erneuernAb = strtotime((string)($lease['refreshAfter'] ?? '')) ?: 0;
|
||||||
|
if (!$erzwingen && $lokalGueltig && time() < $erneuernAb) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!$erzwingen && time() < $this->ReadAttributeInteger('LizenzNaechsterVersuch')) {
|
||||||
|
return $lokalGueltig;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'LizenzNaechsterVersuch',
|
||||||
|
time() + self::LIZENZ_RETRY_SEKUNDEN
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
$neueLease = $this->RufeLizenzserverAuf($code, $installationID);
|
||||||
|
$this->PruefeLizenzLease($neueLease, $installationID);
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLease',
|
||||||
|
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
|
||||||
|
$this->WriteAttributeString('LizenzLetzterFehler', '');
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'LizenzNaechsterVersuch',
|
||||||
|
strtotime((string)$neueLease['refreshAfter']) ?: time() + 86400
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLetzterFehler',
|
||||||
|
substr($fehler->getMessage(), 0, 500)
|
||||||
|
);
|
||||||
|
return $lokalGueltig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function AktualisiereLizenzForm(?string $lizenzcode = null): void
|
||||||
|
{
|
||||||
|
$this->UpdateFormField(
|
||||||
|
'LizenzInstallationID',
|
||||||
|
'caption',
|
||||||
|
'Installations-ID: ' . $this->ReadAttributeString('LizenzInstallationID')
|
||||||
|
);
|
||||||
|
$this->UpdateFormField(
|
||||||
|
'LizenzInformation',
|
||||||
|
'caption',
|
||||||
|
$this->LizenzStatustext($lizenzcode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LizenzStatustext(?string $lizenzcode = null): string
|
||||||
|
{
|
||||||
|
$code = $this->NormalisiereLizenzcode(
|
||||||
|
$lizenzcode ?? $this->ReadPropertyString('Lizenzcode')
|
||||||
|
);
|
||||||
|
if ($code === '') {
|
||||||
|
return 'Verbrauchskostenlizenzcode fehlt.';
|
||||||
|
}
|
||||||
|
if (!$this->IstLizenzcodeGueltig($code)) {
|
||||||
|
return 'Verbrauchskostenlizenzcode ist ungültig.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = $this->ReadAttributeString('LizenzcodeHash');
|
||||||
|
$fehler = $this->ReadAttributeString('LizenzLetzterFehler');
|
||||||
|
if ($hash === '' || !hash_equals($hash, hash('sha256', $code))) {
|
||||||
|
return $fehler !== '' ? 'Nicht freigegeben: ' . $fehler : 'Lizenz noch nicht bestätigt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lease = $this->LeseLizenzLease();
|
||||||
|
$this->PruefeLizenzLease(
|
||||||
|
$lease,
|
||||||
|
$this->ReadAttributeString('LizenzInstallationID')
|
||||||
|
);
|
||||||
|
$status = sprintf(
|
||||||
|
'Aktiv: %d Stromzähler und %d Wärme-/Wasserzähler lizenziert.',
|
||||||
|
$this->LizenzMenge($lease, self::KATALOG_STROMZAEHLER),
|
||||||
|
$this->LizenzMenge($lease, self::KATALOG_NEBENZAEHLER)
|
||||||
|
);
|
||||||
|
if ($fehler !== '') {
|
||||||
|
$status .= ' Online-Prüfung fehlgeschlagen; lokale Freigabe ist noch gültig. Fehler: '
|
||||||
|
. $fehler;
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
} catch (Throwable $ungueltig) {
|
||||||
|
return 'Nicht freigegeben: ' . $ungueltig->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function PruefeLizenzLease(array $lease, string $installationID): void
|
||||||
|
{
|
||||||
|
if (($lease['status'] ?? null) !== 'active') {
|
||||||
|
throw new RuntimeException('Die Verbrauchskostenlizenz ist nicht aktiv.');
|
||||||
|
}
|
||||||
|
if (($lease['installationId'] ?? null) !== $installationID) {
|
||||||
|
throw new RuntimeException('Die Verbrauchskostenlizenz gehört zu einer anderen Installation.');
|
||||||
|
}
|
||||||
|
$offlineBis = strtotime((string)($lease['offlineUntil'] ?? ''));
|
||||||
|
if ($offlineBis === false || time() >= $offlineBis) {
|
||||||
|
throw new RuntimeException('Die Offline-Freigabe ist abgelaufen.');
|
||||||
|
}
|
||||||
|
if ($this->LizenzMenge($lease, self::KATALOG_GRUNDLIZENZ) < 1) {
|
||||||
|
throw new RuntimeException('Die Grundlizenz für den Verbrauchskostenreport fehlt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strom = count($this->LeseJsonListe('PowerMeters'));
|
||||||
|
$neben = count($this->LeseJsonListe('WaterMeters'));
|
||||||
|
$stromLizenziert = $this->LizenzMenge($lease, self::KATALOG_STROMZAEHLER);
|
||||||
|
$nebenLizenziert = $this->LizenzMenge($lease, self::KATALOG_NEBENZAEHLER);
|
||||||
|
if ($strom > $stromLizenziert) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Stromzähler: %d konfiguriert, aber nur %d lizenziert.',
|
||||||
|
$strom,
|
||||||
|
$stromLizenziert
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if ($neben > $nebenLizenziert) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Wärme-/Wasserzähler: %d konfiguriert, aber nur %d lizenziert.',
|
||||||
|
$neben,
|
||||||
|
$nebenLizenziert
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LizenzMenge(array $lease, string $kennung): int
|
||||||
|
{
|
||||||
|
$menge = 0;
|
||||||
|
foreach (($lease['entitlements'] ?? []) as $berechtigung) {
|
||||||
|
if (!is_array($berechtigung) || ($berechtigung['status'] ?? null) !== 'active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$schluessel = $berechtigung['catalog_key'] ?? $berechtigung['sku'] ?? null;
|
||||||
|
$anzahl = $berechtigung['quantity'] ?? 0;
|
||||||
|
if ($schluessel === $kennung && is_int($anzahl) && $anzahl > 0) {
|
||||||
|
$menge += $anzahl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $menge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LeseJsonListe(string $property): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$werte = json_decode(
|
||||||
|
$this->ReadPropertyString($property),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
return is_array($werte) ? array_values($werte) : [];
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LeseLizenzLease(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$lease = json_decode(
|
||||||
|
$this->ReadAttributeString('LizenzLease'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
return is_array($lease) ? $lease : [];
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function RufeLizenzserverAuf(string $lizenzcode, string $installationID): array
|
||||||
|
{
|
||||||
|
$curl = curl_init(self::LIZENZ_ENDPOINT);
|
||||||
|
if ($curl === false) {
|
||||||
|
throw new RuntimeException('Die HTTPS-Anfrage konnte nicht vorbereitet werden.');
|
||||||
|
}
|
||||||
|
$nutzlast = json_encode([
|
||||||
|
'code' => $lizenzcode,
|
||||||
|
'installationId' => $installationID,
|
||||||
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
try {
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $nutzlast,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json'],
|
||||||
|
CURLOPT_USERAGENT => 'Enelix-Verbrauchskostenreport/0.1',
|
||||||
|
]);
|
||||||
|
$antwort = curl_exec($curl);
|
||||||
|
if ($antwort === false) {
|
||||||
|
throw new RuntimeException('Lizenzserver nicht erreichbar: ' . curl_error($curl));
|
||||||
|
}
|
||||||
|
if (strlen($antwort) > 65536) {
|
||||||
|
throw new RuntimeException('Die Antwort des Lizenzservers ist zu gross.');
|
||||||
|
}
|
||||||
|
$httpStatus = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
} finally {
|
||||||
|
curl_close($curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($daten)) {
|
||||||
|
throw new RuntimeException('Der Lizenzserver lieferte kein JSON-Objekt.');
|
||||||
|
}
|
||||||
|
if ($httpStatus !== 200) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Lizenzserver HTTP ' . $httpStatus . ': '
|
||||||
|
. (is_string($daten['error'] ?? null) ? $daten['error'] : 'Unbekannter Fehler')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function NormalisiereLizenzcode(string $code): string
|
||||||
|
{
|
||||||
|
return strtoupper((string)preg_replace('/[^A-Z0-9]/i', '', $code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function IstLizenzcodeGueltig(string $code): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^ENX[A-HJ-NP-Z2-9]{16}$/', $code) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ErzeugeUuidV4(): string
|
||||||
|
{
|
||||||
|
$bytes = random_bytes(16);
|
||||||
|
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||||
|
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||||
|
$hex = bin2hex($bytes);
|
||||||
|
return sprintf(
|
||||||
|
'%s-%s-%s-%s-%s',
|
||||||
|
substr($hex, 0, 8),
|
||||||
|
substr($hex, 8, 4),
|
||||||
|
substr($hex, 12, 4),
|
||||||
|
substr($hex, 16, 4),
|
||||||
|
substr($hex, 20, 12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function ReadUsers()
|
private function ReadUsers()
|
||||||
{
|
{
|
||||||
$users = json_decode($this->ReadPropertyString('Users'), true);
|
$users = json_decode($this->ReadPropertyString('Users'), true);
|
||||||
@@ -1348,6 +1638,14 @@ refreshPickers();
|
|||||||
public function RequestAction($Ident, $Value)
|
public function RequestAction($Ident, $Value)
|
||||||
{
|
{
|
||||||
switch ($Ident) {
|
switch ($Ident) {
|
||||||
|
case 'FormLizenzPruefen':
|
||||||
|
if (!is_string($Value)) {
|
||||||
|
throw new InvalidArgumentException('Der Lizenzcode muss als Text übergeben werden.');
|
||||||
|
}
|
||||||
|
$this->AktualisiereLizenz($Value, true);
|
||||||
|
$this->AktualisiereLizenzForm($Value);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'FromDate':
|
case 'FromDate':
|
||||||
case 'ToDate':
|
case 'ToDate':
|
||||||
case 'VisuTariffType':
|
case 'VisuTariffType':
|
||||||
@@ -1458,6 +1756,10 @@ refreshPickers();
|
|||||||
|
|
||||||
public function GenerateReport()
|
public function GenerateReport()
|
||||||
{
|
{
|
||||||
|
if (!$this->AktualisiereLizenz($this->ReadPropertyString('Lizenzcode'), false)) {
|
||||||
|
throw new RuntimeException($this->LizenzStatustext());
|
||||||
|
}
|
||||||
|
|
||||||
$from = GetValue($this->GetIDForIdent('FromDate'));
|
$from = GetValue($this->GetIDForIdent('FromDate'));
|
||||||
$to = GetValue($this->GetIDForIdent('ToDate'));
|
$to = GetValue($this->GetIDForIdent('ToDate'));
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "belevo/enelix-utils",
|
"name": "belevo/enelix-utils",
|
||||||
"description": "Unabhaengige Zusatzmodule fuer IP-Symcon",
|
"description": "Unabhaengige Zusatzmodule fuer IP-Symcon",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
|
"license": "proprietary",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.0"
|
"php": ">=8.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
Der Modulpräfix lautet `VKR`. Der Report wird über die Aktion `CreateReport`
|
Der Modulpräfix lautet `VKR`. Der Report wird über die Aktion `CreateReport`
|
||||||
erstellt und im Medium `ReportPDF` bereitgestellt.
|
erstellt und im Medium `ReportPDF` bereitgestellt.
|
||||||
|
|
||||||
|
## Lizenzierung
|
||||||
|
|
||||||
|
Die Freigabe besteht aus einer Grundlizenz und getrennten Kontingenten fuer
|
||||||
|
Stromzaehler und Nebenzaehler. Der konfigurierte Umfang darf die vom
|
||||||
|
Lizenzdienst bestaetigten Mengen nicht ueberschreiten.
|
||||||
|
|
||||||
## Variablen
|
## Variablen
|
||||||
|
|
||||||
| Technischer Ident / Anzeige | Typ / Zugriff | Beschreibung |
|
| Technischer Ident / Anzeige | Typ / Zugriff | Beschreibung |
|
||||||
@@ -28,6 +34,7 @@ erstellt und im Medium `ReportPDF` bereitgestellt.
|
|||||||
|
|
||||||
| Technischer Ident / Anzeige | Typ | Standard / Beschreibung |
|
| Technischer Ident / Anzeige | Typ | Standard / Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `Lizenzcode` / Lizenzcode | String | leer; wird zur Laufzeit am Lizenzdienst geprueft. |
|
||||||
| `Users` / Benutzer | String/JSON | `[]`; Benutzerstruktur. |
|
| `Users` / Benutzer | String/JSON | `[]`; Benutzerstruktur. |
|
||||||
| `PowerMeters` / Stromzähler | String/JSON | `[]`; Zuordnungen. |
|
| `PowerMeters` / Stromzähler | String/JSON | `[]`; Zuordnungen. |
|
||||||
| `WaterMeters` / Wasserzähler | String/JSON | `[]`; Zuordnungen. |
|
| `WaterMeters` / Wasserzähler | String/JSON | `[]`; Zuordnungen. |
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ werden, wenn sie vor dem Lauf nicht existierten.
|
|||||||
- `affected`: die durch geänderte Pfade ermittelten Module
|
- `affected`: die durch geänderte Pfade ermittelten Module
|
||||||
|
|
||||||
Verfügbare Module: `CC100Hardware`, `Energiediagramm`, `ShellyModul`,
|
Verfügbare Module: `CC100Hardware`, `Energiediagramm`, `ShellyModul`,
|
||||||
`Verbrauchskostenreport`.
|
`VGTSchnittstelle`, `Verbrauchskostenreport` und `VirtuelleBatterie`.
|
||||||
|
|
||||||
Die Utils-Module werden jeweils eigenständig und ohne Abhängigkeit zu Enelix EMS getestet.
|
Die Utils-Module werden jeweils eigenständig und ohne Abhängigkeit zu Enelix EMS getestet.
|
||||||
|
|
||||||
@@ -84,15 +84,15 @@ Zusätzlich schreibt der Runner die Zusammenfassung in das IP-Symcon-Log.
|
|||||||
|
|
||||||
## CI-Regeln
|
## CI-Regeln
|
||||||
|
|
||||||
Bei jedem Push laufen sämtliche PHPUnit-Tests. Auf `develop` werden zusätzlich
|
Bei jedem Push laufen sämtliche PHPUnit-Tests und die PHP-Syntaxprüfung. Die
|
||||||
die von den geänderten Pfaden betroffenen Symcon-Modultests ausgeführt. Bei
|
Symcon-Modultests werden bis zur Verfügbarkeit eines geschützten Runners auf
|
||||||
gemeinsamen Framework- oder Vertragsänderungen läuft die vollständige Suite.
|
der isolierten IP-Symcon-8.0-Instanz des Agent-Servers ausgeführt. Vor jeder
|
||||||
Für `beta` ist immer `all` vorgeschrieben.
|
Übernahme nach `beta` ist ein erfolgreicher Lauf im Modus `all` verbindlich.
|
||||||
|
Der erzeugte JSON- und JUnit-Bericht gehört zum Freigabenachweis.
|
||||||
|
|
||||||
Die Symcon-Jobs benötigen einen geschützten Runner mit dem Label `symcon-8`,
|
Eine spätere CI-Automatisierung benötigt einen geschützten Runner mit dem Label
|
||||||
lokalem Zugriff auf eine isolierte IP-Symcon-8.x-Instanz und den installierten
|
`symcon-8`, lokalem Zugriff auf die isolierte IP-Symcon-Instanz und den exakt
|
||||||
Repository-Stand. Ohne diesen Runner ist die Richtlinie dokumentiert, aber
|
zu prüfenden Repository-Stand.
|
||||||
nicht technisch durchsetzbar.
|
|
||||||
|
|
||||||
## Checkliste für neue Module
|
## Checkliste für neue Module
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "{05DE4157-F0E8-4CA9-8B5E-D6AB9310775F}",
|
"id": "{05DE4157-F0E8-4CA9-8B5E-D6AB9310775F}",
|
||||||
"name": "Enelix Utils",
|
"name": "Enelix Utils",
|
||||||
"author": "Belevo AG",
|
"author": "Enelix",
|
||||||
"url": "https://git.belevo.ch/ENELIX/Enelix-Utils",
|
"url": "https://git.belevo.ch/ENELIX/Enelix-Utils",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"version": "8.0"
|
"version": "8.0"
|
||||||
},
|
},
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"build": 0,
|
"build": 1,
|
||||||
"date": 0
|
"date": 1790662541
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
'EinspeisungZaehlerID',
|
'EinspeisungZaehlerID',
|
||||||
'NetzbezugZaehlerID',
|
'NetzbezugZaehlerID',
|
||||||
'VerbrauchZaehlerID',
|
'VerbrauchZaehlerID',
|
||||||
|
'EnergieanteileBerechenbar',
|
||||||
'Standardzeitraum',
|
'Standardzeitraum',
|
||||||
'FarbeEigenverbrauch',
|
'FarbeEigenverbrauch',
|
||||||
'FarbeAutarkie',
|
'FarbeAutarkie',
|
||||||
@@ -49,11 +50,40 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('__ENELIX_INITIAL_STATE__', $html);
|
self::assertStringContainsString('__ENELIX_INITIAL_STATE__', $html);
|
||||||
self::assertStringContainsString('ResizeObserver', $html);
|
self::assertStringContainsString('ResizeObserver', $html);
|
||||||
self::assertStringContainsString('grid-template-columns: minmax(0, 1fr)', $html);
|
self::assertStringContainsString('grid-template-columns: minmax(0, 1fr)', $html);
|
||||||
|
self::assertStringContainsString('padding: 58px 14px 14px', $html);
|
||||||
|
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('SetCustomRange', $html);
|
self::assertStringContainsString('SetCustomRange', $html);
|
||||||
self::assertStringNotContainsString("addEventListener('load'", $html);
|
self::assertStringNotContainsString("addEventListener('load'", $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testModuleEmbedsInitialPayloadForTileAndPopup(): void
|
public function testOriginalEnergyPieBackgroundStaysReadableAndNonInteractive(): void
|
||||||
|
{
|
||||||
|
$html = file_get_contents(__DIR__ . '/../Energiediagramm/module.html');
|
||||||
|
self::assertNotFalse($html);
|
||||||
|
self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*--card-color: #121216;/s', $html);
|
||||||
|
self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*--content-color: #f5f5f7;/s', $html);
|
||||||
|
self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*color-scheme: dark;/s', $html);
|
||||||
|
self::assertMatchesRegularExpression('/#energy-app\\s*\\{[^}]*isolation: isolate;/s', $html);
|
||||||
|
self::assertSame(1, preg_match('/#energy-app::before\\s*\\{([^}]+)\\}/s', $html, $background));
|
||||||
|
foreach ([
|
||||||
|
'inset: 0',
|
||||||
|
'z-index: -1',
|
||||||
|
'radial-gradient(520px 420px at 18% 18%, rgba(99, 179, 255, 0.42), transparent 60%)',
|
||||||
|
'radial-gradient(560px 460px at 82% 28%, rgba(168, 85, 247, 0.38), transparent 62%)',
|
||||||
|
'radial-gradient(620px 520px at 55% 85%, rgba(99, 179, 255, 0.26), transparent 64%)',
|
||||||
|
'filter: blur(18px)',
|
||||||
|
'pointer-events: none',
|
||||||
|
] as $style) {
|
||||||
|
self::assertStringContainsString($style, $background[1]);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString('overflow: auto', $html);
|
||||||
|
self::assertStringNotContainsString('touch-action: none', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModuleEmbedsInitialPayloadAndEnablesSupportedVisualization(): void
|
||||||
{
|
{
|
||||||
$module = file_get_contents(__DIR__ . '/../Energiediagramm/module.php');
|
$module = file_get_contents(__DIR__ . '/../Energiediagramm/module.php');
|
||||||
self::assertNotFalse($module);
|
self::assertNotFalse($module);
|
||||||
@@ -61,7 +91,11 @@ final class EnergiediagrammModulstrukturTest extends TestCase
|
|||||||
"str_replace('__ENELIX_INITIAL_STATE__'",
|
"str_replace('__ENELIX_INITIAL_STATE__'",
|
||||||
$module
|
$module
|
||||||
);
|
);
|
||||||
self::assertStringContainsString('GetVisualizationPopup', $module);
|
self::assertStringContainsString(
|
||||||
|
"version_compare(IPS_GetKernelVersion(), '9.0', '>=')",
|
||||||
|
$module
|
||||||
|
);
|
||||||
|
self::assertStringContainsString('SetVisualizationType($visualizationType)', $module);
|
||||||
self::assertStringContainsString('Standardzeitraum', $module);
|
self::assertStringContainsString('Standardzeitraum', $module);
|
||||||
self::assertStringContainsString('CustomStart', $module);
|
self::assertStringContainsString('CustomStart', $module);
|
||||||
self::assertStringContainsString('CustomEnd', $module);
|
self::assertStringContainsString('CustomEnd', $module);
|
||||||
|
|||||||
@@ -110,9 +110,45 @@ final class EnergiediagrammTest extends TestCase
|
|||||||
'#2F80ED',
|
'#2F80ED',
|
||||||
EnergyChartCalculator::normalizeColor('#2f80ed', '#000000')
|
EnergyChartCalculator::normalizeColor('#2f80ed', '#000000')
|
||||||
);
|
);
|
||||||
|
self::assertSame(
|
||||||
|
'#00FF4C',
|
||||||
|
EnergyChartCalculator::normalizeColor('65356', '#000000')
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
'#000000',
|
||||||
|
EnergyChartCalculator::normalizeColor('0', '#FFFFFF')
|
||||||
|
);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
'#22A06B',
|
'#22A06B',
|
||||||
EnergyChartCalculator::normalizeColor('transparent', '#22a06b')
|
EnergyChartCalculator::normalizeColor('16777216', '#22a06b')
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
'#22A06B',
|
||||||
|
EnergyChartCalculator::normalizeColor('-1', '#22a06b')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMixedMeasurementBoundariesKeepTotalsWithoutInventingShares(): void
|
||||||
|
{
|
||||||
|
$values = EnergyChartCalculator::metrics(10.0, 0.0, 10.0, 5.0, false);
|
||||||
|
|
||||||
|
self::assertSame(10.0, $values['Produktion']);
|
||||||
|
self::assertSame(0.0, $values['Einspeisung']);
|
||||||
|
self::assertSame(10.0, $values['Netzbezug']);
|
||||||
|
self::assertSame(5.0, $values['Hausverbrauch']);
|
||||||
|
self::assertNull($values['Eigenverbrauch']);
|
||||||
|
self::assertNull($values['Eigenverbrauchsquote']);
|
||||||
|
self::assertNull($values['Autarkiegrad']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnavailableSharesRemainNullWithZeroEnergy(): void
|
||||||
|
{
|
||||||
|
$unavailable = EnergyChartCalculator::metrics(0.0, 0.0, 0.0, null, false);
|
||||||
|
self::assertNull($unavailable['Eigenverbrauchsquote']);
|
||||||
|
self::assertNull($unavailable['Autarkiegrad']);
|
||||||
|
|
||||||
|
$available = EnergyChartCalculator::metrics(0.0, 0.0, 0.0);
|
||||||
|
self::assertSame(0.0, $available['Eigenverbrauchsquote']);
|
||||||
|
self::assertSame(0.0, $available['Autarkiegrad']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
function visualization(initialState) {
|
||||||
|
const elements = new Map();
|
||||||
|
const element = id => {
|
||||||
|
if (!elements.has(id)) {
|
||||||
|
elements.set(id, {
|
||||||
|
innerHTML: '', textContent: '', value: '', hidden: false,
|
||||||
|
addEventListener() {}, classList: { toggle() {} }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return elements.get(id);
|
||||||
|
};
|
||||||
|
const window = { setTimeout() {} };
|
||||||
|
const document = {
|
||||||
|
getElementById: element,
|
||||||
|
documentElement: { style: {} },
|
||||||
|
addEventListener() {}
|
||||||
|
};
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '../Energiediagramm/module.html'), 'utf8');
|
||||||
|
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||||
|
.replace('__ENELIX_INITIAL_STATE__', JSON.stringify(initialState));
|
||||||
|
vm.runInNewContext(script, {
|
||||||
|
window, document,
|
||||||
|
getComputedStyle: () => ({ getPropertyValue: () => '' })
|
||||||
|
});
|
||||||
|
return { element, window };
|
||||||
|
}
|
||||||
|
|
||||||
|
function payload(overrides = {}) {
|
||||||
|
return {
|
||||||
|
range: 'day', date: '2026-10-06', hasData: true,
|
||||||
|
values: {
|
||||||
|
Produktion: 10, Hausverbrauch: 5, Netzbezug: 10, Einspeisung: 0,
|
||||||
|
Eigenverbrauchsquote: null, Autarkiegrad: null
|
||||||
|
},
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('unavailable shares show neutral rings while preserving energy totals', () => {
|
||||||
|
const ui = visualization(payload({ sharesAvailable: false, attributionHint: 'Different measurement boundaries.' }));
|
||||||
|
for (const id of ['self-consumption-chart', 'autarky-chart']) {
|
||||||
|
const html = ui.element(id).innerHTML;
|
||||||
|
assert.match(html, /Nicht bestimmbar/);
|
||||||
|
assert.doesNotMatch(html, /donut-value|0\.0%/);
|
||||||
|
}
|
||||||
|
assert.match(ui.element('summary').innerHTML, /Produktion/);
|
||||||
|
assert.match(ui.element('summary').innerHTML, /10\.00 kWh/);
|
||||||
|
assert.match(ui.element('summary').innerHTML, /5\.00 kWh/);
|
||||||
|
assert.equal(ui.element('message').hidden, false);
|
||||||
|
assert.equal(ui.element('message').textContent, 'Different measurement boundaries.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unavailable flag cannot display stale numeric percentages', () => {
|
||||||
|
const state = payload({ sharesAvailable: false });
|
||||||
|
state.values.Eigenverbrauchsquote = 75;
|
||||||
|
state.values.Autarkiegrad = 50;
|
||||||
|
const ui = visualization(state);
|
||||||
|
assert.doesNotMatch(ui.element('self-consumption-chart').innerHTML, /75\.0%/);
|
||||||
|
assert.doesNotMatch(ui.element('autarky-chart').innerHTML, /50\.0%/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('legacy numeric percentages and later recovery remain compatible', () => {
|
||||||
|
const ui = visualization(payload({ sharesAvailable: false }));
|
||||||
|
const state = payload();
|
||||||
|
state.values.Eigenverbrauchsquote = 75;
|
||||||
|
state.values.Autarkiegrad = 81.25;
|
||||||
|
ui.window.handleMessage(JSON.stringify(state));
|
||||||
|
assert.match(ui.element('self-consumption-chart').innerHTML, /75\.0%/);
|
||||||
|
assert.match(ui.element('autarky-chart').innerHTML, /81\.3%/);
|
||||||
|
assert.equal(ui.element('message').hidden, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing archive values do not hide the attribution limitation', () => {
|
||||||
|
const ui = visualization(payload({
|
||||||
|
hasData: false, noDataHint: 'No archive data.',
|
||||||
|
sharesAvailable: false, attributionHint: 'Different measurement boundaries.'
|
||||||
|
}));
|
||||||
|
assert.equal(ui.element('message').textContent,
|
||||||
|
'No archive data. Different measurement boundaries.');
|
||||||
|
});
|
||||||
@@ -15,7 +15,11 @@ final class LibraryMetadataTest extends TestCase
|
|||||||
|
|
||||||
$daten = json_decode($inhalt, true, 512, JSON_THROW_ON_ERROR);
|
$daten = json_decode($inhalt, true, 512, JSON_THROW_ON_ERROR);
|
||||||
self::assertSame('Enelix Utils', $daten['name']);
|
self::assertSame('Enelix Utils', $daten['name']);
|
||||||
|
self::assertSame('Enelix', $daten['author']);
|
||||||
self::assertSame('8.0', $daten['compatibility']['version']);
|
self::assertSame('8.0', $daten['compatibility']['version']);
|
||||||
|
self::assertSame('0.1', $daten['version']);
|
||||||
|
self::assertSame(1, $daten['build']);
|
||||||
|
self::assertGreaterThan(0, $daten['date']);
|
||||||
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $daten['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user