Author SHA1 Message Date
dh 59eec738ab chore(release): erste Beta vorbereiten
Tests / test (push) Successful in 51s
2026-09-29 06:25:29 +00:00
dh 40c6a17dd1 fix(energiediagramm): Symcon-8-Darstellung korrigieren 2026-09-29 06:24:29 +00:00
dh 1f945be8f1 feat: Verbrauchskostenreport lizenzieren
Tests / test (push) Successful in 45s
2026-09-27 12:33:36 +00:00
17 changed files with 463 additions and 28 deletions
+8
View File
@@ -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
+26
View File
@@ -0,0 +1,26 @@
# Changelog
Alle wesentlichen Aenderungen an Enelix Utils werden in dieser Datei dokumentiert.
## 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
- 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.
+12 -5
View File
@@ -24,14 +24,21 @@ 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. Beim Oeffnen und beim erneuten
die Darstellung aktualisiert. Das Diagrammraster wechselt bei geringer Breite Sichtbarwerden wird die Darstellung aktualisiert. Das Diagrammraster wechselt
automatisch von zwei Spalten auf eine Spalte. 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
+13 -3
View File
@@ -118,9 +118,19 @@ final class EnergyChartCalculator
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
+2 -2
View File
@@ -71,7 +71,7 @@
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: inherit;
@@ -234,7 +234,7 @@
@media (max-width: 560px) { @media (max-width: 560px) {
#energy-app { #energy-app {
padding: 10px; padding: 58px 10px 10px;
} }
.toolbar, .toolbar,
+4 -1
View File
@@ -37,7 +37,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,
+9 -3
View File
@@ -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
+8
View File
@@ -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 |
+36
View File
@@ -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"
+302
View File
@@ -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'));
+1
View File
@@ -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. |
+9 -9
View File
@@ -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
+2 -2
View File
@@ -7,6 +7,6 @@
"version": "8.0" "version": "8.0"
}, },
"version": "0.1", "version": "0.1",
"build": 0, "build": 1,
"date": 0 "date": 1790662541
} }
+8 -2
View File
@@ -49,11 +49,13 @@ 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('SetCustomRange', $html); self::assertStringContainsString('SetCustomRange', $html);
self::assertStringNotContainsString("addEventListener('load'", $html); self::assertStringNotContainsString("addEventListener('load'", $html);
} }
public function testModuleEmbedsInitialPayloadForTileAndPopup(): void 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 +63,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);
+13 -1
View File
@@ -110,9 +110,21 @@ 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')
); );
} }
} }
+3
View File
@@ -16,6 +16,9 @@ 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('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']);
} }