Manager-Lizenzierung integrieren
Tests / test (push) Successful in 1m7s

This commit is contained in:
dh
2026-09-20 14:21:11 +00:00
parent cf1ae5e348
commit 7b6d1b2f07
11 changed files with 644 additions and 23 deletions
+22 -3
View File
@@ -3,11 +3,29 @@
{
"type": "ExpansionPanel",
"caption": "Lizenzierung",
"expanded": true,
"items": [
{
"type": "ValidationTextBox",
"name": "LizenzAnschluss",
"caption": "Konfiguration als JSON"
"name": "Lizenzcode",
"caption": "Lizenzcode",
"placeholder": "ENX-XXXX-XXXX-XXXX-XXXX"
},
{
"type": "Label",
"name": "LizenzInstallationID",
"caption": "Installations-ID wird erzeugt"
},
{
"type": "Label",
"name": "LizenzInformation",
"caption": "Lizenzcode fehlt."
},
{
"type": "Button",
"name": "LizenzPruefen",
"caption": "Lizenz pruefen und binden",
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
}
]
},
@@ -277,6 +295,7 @@
"status": [
{"code": 102, "icon": "active", "caption": "Aktiv"},
{"code": 201, "icon": "inactive", "caption": "Netzleistungsmessung fehlt oder ist veraltet"},
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"}
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
{"code": 203, "icon": "error", "caption": "Lizenz fehlt oder ist abgelaufen"}
]
}
+244 -2
View File
@@ -5,7 +5,9 @@ declare(strict_types=1);
require_once __DIR__ . '/../libs/ManagerSchnittstelle.php';
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
require_once __DIR__ . '/../libs/ManagerRegler.php';
require_once __DIR__ . '/../libs/Lizenzpruefung.php';
use Belevo\EnelixEMS\Lizenzpruefung;
use Belevo\EnelixEMS\ManagerRegler;
use Belevo\EnelixEMS\ManagerSchnittstelle;
use Belevo\EnelixEMS\Nachrichtenvertrag;
@@ -15,6 +17,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201;
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
private const STATUS_LIZENZ_UNGUELTIG = 203;
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
private const LIZENZ_RETRY_SEKUNDEN = 3600;
private const VM_UPDATE = 10603;
private const VERBRAUCHER_MODUL_IDS = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
@@ -61,6 +66,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyFloat('Umschaltdifferenz', 5.0);
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyString('Lizenzcode', '');
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
$this->RegisterPropertyString('LizenzAnschluss', '{}');
$this->RegisterPropertyString('StoermeldeAnschluss', '{}');
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
@@ -74,6 +81,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeString('VerbraucherCache', '{}');
$this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0);
$this->RegisterAttributeString('LizenzInstallationID', '');
$this->RegisterAttributeString('LizenzLease', '{}');
$this->RegisterAttributeString('LizenzcodeHash', '');
$this->RegisterAttributeInteger('LizenzNaechsterVersuch', 0);
$this->RegisterAttributeString('LizenzLetzterFehler', '');
if ($this->ReadAttributeString('LizenzInstallationID') === '') {
$this->WriteAttributeString('LizenzInstallationID', $this->erzeugeUuidV4());
}
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, '');
@@ -108,6 +123,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereAnschlussstatus();
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
$this->SetStatus(self::STATUS_AKTIV);
}
@@ -124,6 +144,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$monatsgrenzenSichtbar = $lastspitzenmodus === 2
&& $this->GetBuffer('MonatsgrenzenSichtbar') === '1';
$this->setzeFormularParameter(
$formular['elements'],
'LizenzInstallationID',
'caption',
'Installations-ID: ' . $this->ReadAttributeString('LizenzInstallationID')
);
$this->setzeFormularParameter(
$formular['elements'],
'LizenzInformation',
'caption',
$this->lizenzStatustext()
);
$this->setzeFormularParameter($formular['elements'], 'VerbraucherZuordnung', 'visible', !$automatisch);
$this->setzeFormularParameter(
$formular['elements'],
@@ -199,6 +231,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->regeln((bool) $wert);
return;
case 'FormLizenzPruefen':
if (!is_string($wert)) {
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
}
$this->aktualisiereLizenzfreigabe($wert, true);
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
return;
case 'FormAutomatischeSuche':
$automatisch = (bool) $wert;
$this->UpdateFormField('VerbraucherZuordnung', 'visible', !$automatisch);
@@ -298,6 +338,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function regeln(bool $erneuern): void
{
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
[$netzleistung, $netzleistungGueltig] = $this->leseNetzleistung();
$this->SetValue('Netzleistung', $netzleistung);
$this->setzeDiagnosewert('NetzleistungGueltig', $netzleistungGueltig);
@@ -685,12 +732,207 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
}
private function aktualisiereLizenzfreigabe(string $lizenzcode, bool $erzwingen): bool
{
$normalisiert = Lizenzpruefung::normalisiereCode($lizenzcode);
if (!Lizenzpruefung::istLizenzcodeGueltig($normalisiert)) {
$fehler = $normalisiert === '' ? 'Lizenzcode fehlt.' : 'Lizenzcode ist ungueltig.';
$this->WriteAttributeString('LizenzLetzterFehler', $fehler);
$this->setzeDiagnosewert('Lizenzstatus', $fehler);
return false;
}
$installationID = $this->ReadAttributeString('LizenzInstallationID');
if ($installationID === '') {
$installationID = $this->erzeugeUuidV4();
$this->WriteAttributeString('LizenzInstallationID', $installationID);
}
$codeHash = hash('sha256', $normalisiert);
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
$lease = $this->leseLizenzLease();
$codePasst = $gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash);
$gueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
$lease,
$installationID,
$this->ReadPropertyInteger('Lastspitzenmodus')
);
$faellig = !$gueltig || Lizenzpruefung::erneuerungFaellig($lease);
$jetzt = time();
if (!$erzwingen
&& (!$faellig || $jetzt < $this->ReadAttributeInteger('LizenzNaechsterVersuch'))
) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
return $gueltig;
}
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $jetzt + self::LIZENZ_RETRY_SEKUNDEN);
try {
$neueLease = $this->rufeLizenzserverAuf($normalisiert, $installationID);
Lizenzpruefung::pruefeLease(
$neueLease,
$installationID,
$this->ReadPropertyInteger('Lastspitzenmodus')
);
$this->WriteAttributeString(
'LizenzLease',
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
);
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
$this->WriteAttributeInteger(
'LizenzNaechsterVersuch',
Lizenzpruefung::erneuernAb($neueLease)
);
$this->WriteAttributeString('LizenzLetzterFehler', '');
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
return true;
} catch (Throwable $fehler) {
$meldung = substr($fehler->getMessage(), 0, 500);
$this->WriteAttributeString('LizenzLetzterFehler', $meldung);
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
$this->protokolliere('Lizenzpruefung', $meldung);
return $gueltig;
}
}
/** @return array<string, mixed> */
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 lizenzStatustext(?string $lizenzcode = null): string
{
$code = Lizenzpruefung::normalisiereCode(
$lizenzcode ?? $this->ReadPropertyString('Lizenzcode')
);
if ($code === '') {
return 'Lizenzcode fehlt.';
}
if (!Lizenzpruefung::istLizenzcodeGueltig($code)) {
return 'Lizenzcode ist ungueltig.';
}
$codeHash = hash('sha256', $code);
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
$fehler = $this->ReadAttributeString('LizenzLetzterFehler');
if ($gespeicherterHash === '' || !hash_equals($gespeicherterHash, $codeHash)) {
return $fehler !== '' ? 'Nicht freigegeben: ' . $fehler : 'Lizenz noch nicht bestaetigt.';
}
$lease = $this->leseLizenzLease();
try {
Lizenzpruefung::pruefeLease(
$lease,
$this->ReadAttributeString('LizenzInstallationID'),
$this->ReadPropertyInteger('Lastspitzenmodus')
);
$status = ($lease['development'] ?? false) === true
? 'Aktiv (Entwicklung)'
: 'Aktiv';
$status .= ' bis ' . date('d.m.Y H:i', Lizenzpruefung::offlineBis($lease));
if ($fehler !== '') {
$status .= '; Erneuerung fehlgeschlagen: ' . $fehler;
}
return $status;
} catch (Throwable $ungueltig) {
return 'Nicht freigegeben: ' . $ungueltig->getMessage();
}
}
/** @return array<string, mixed> */
private function rufeLizenzserverAuf(string $lizenzcode, string $installationID): array
{
$nutzlast = json_encode([
'code' => $lizenzcode,
'installationId' => $installationID,
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$curl = curl_init(self::LIZENZ_ENDPOINT);
if ($curl === false) {
throw new RuntimeException('Die HTTPS-Anfrage konnte nicht vorbereitet werden.');
}
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-EMS-Manager/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);
}
try {
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
} catch (Throwable $fehler) {
throw new RuntimeException('Der Lizenzserver lieferte kein gueltiges JSON.', 0, $fehler);
}
if (!is_array($daten)) {
throw new RuntimeException('Der Lizenzserver lieferte kein JSON-Objekt.');
}
if ($httpStatus !== 200) {
$meldung = is_string($daten['error'] ?? null)
? $daten['error']
: 'Unbekannter Fehler';
throw new RuntimeException('Lizenzserver HTTP ' . $httpStatus . ': ' . $meldung);
}
return $daten;
}
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 aktualisiereAnschlussstatus(): void
{
foreach ([
'PrognoseAnschluss' => 'Prognosestatus',
'SDLAnschluss' => 'SDLStatus',
'LizenzAnschluss' => 'Lizenzstatus',
'StoermeldeAnschluss' => 'Stoerueberwachungsstatus',
] as $property => $variable) {
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
@@ -714,7 +956,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
foreach (['PrognoseAnschluss', 'SDLAnschluss', 'LizenzAnschluss', 'StoermeldeAnschluss'] as $property) {
foreach (['PrognoseAnschluss', 'SDLAnschluss', 'StoermeldeAnschluss'] as $property) {
$wert = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($wert)) {
throw new InvalidArgumentException($property . ' muss ein JSON-Objekt sein.');