feat: Lizenzkontingente im Manager durchsetzen

This commit is contained in:
dh
2026-09-22 14:43:36 +00:00
parent 7845c66fd8
commit 2392a9c209
6 changed files with 627 additions and 31 deletions
+8 -1
View File
@@ -26,6 +26,13 @@
"name": "LizenzPruefen",
"caption": "Lizenz pruefen und binden",
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
},
{
"type": "Button",
"name": "LizenzVerwalten",
"caption": "Lizenz verwalten",
"link": true,
"onClick": "echo 'https://license.enelix.ch';"
}
]
},
@@ -296,6 +303,6 @@
{"code": 102, "icon": "active", "caption": "Aktiv"},
{"code": 201, "icon": "inactive", "caption": "Netzleistungsmessung fehlt oder ist veraltet"},
{"code": 202, "icon": "error", "caption": "Konfiguration ungueltig"},
{"code": 203, "icon": "error", "caption": "Lizenz fehlt oder ist abgelaufen"}
{"code": 203, "icon": "error", "caption": "Lizenz nicht freigegeben oder Verbraucherkontingent ueberschritten"}
]
}
+437 -29
View File
@@ -21,14 +21,35 @@ class Manager extends IPSModule implements ManagerSchnittstelle
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}',
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
'{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}',
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
'Katalog' => 'boiler_multi',
'Name' => 'Boiler mehrstufig',
],
'{15879A4E-D0C2-4495-83DE-46E1E462591E}' => [
'Katalog' => 'consumer_single',
'Name' => 'Verbraucher einstufig',
],
'{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}' => [
'Katalog' => 'consumer_single',
'Name' => 'Waermepumpe',
],
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}' => [
'Katalog' => 'ev_charger',
'Name' => 'EV-Ladestation Stand-Alone',
],
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}' => [
'Katalog' => 'ev_charger',
'Name' => 'EV-Ladestation Gateway',
],
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}' => [
'Katalog' => 'buffer_storage',
'Name' => 'Pufferspeicher',
],
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}' => [
'Katalog' => 'battery',
'Name' => 'Batteriespeicher',
],
];
private const MONATSNAMEN = [
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
@@ -41,6 +62,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'VerbraucherAnzahl',
'VerbraucherVerfuegbar',
'Verbraucherstatus',
'NichtGeregelteGeraete',
'Monatsgrenzen',
'Prognosestatus',
'SDLStatus',
@@ -90,6 +112,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeString('LizenzLease', '{}');
$this->RegisterAttributeString('LizenzcodeHash', '');
$this->RegisterAttributeInteger('LizenzNaechsterVersuch', 0);
$this->RegisterAttributeInteger('LizenzBerechtigungenNaechsterVersuch', 0);
$this->RegisterAttributeString('LizenzLetzterFehler', '');
if ($this->ReadAttributeString('LizenzInstallationID') === '') {
$this->WriteAttributeString('LizenzInstallationID', $this->erzeugeUuidV4());
@@ -133,6 +156,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
try {
$this->aktiveVerbraucherIDs();
} catch (Throwable $fehler) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->setzeDiagnosewert('Lizenzstatus', 'Nicht freigegeben: ' . $fehler->getMessage());
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
$this->SetStatus(self::STATUS_AKTIV);
}
@@ -146,7 +177,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
);
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
$monatsgrenzenSichtbar = $lastspitzenmodus === 2
$peakShavingLizenziert = $this->peakShavingLizenziert();
$monatsgrenzenSichtbar = $peakShavingLizenziert
&& $lastspitzenmodus === 2
&& $this->GetBuffer('MonatsgrenzenSichtbar') === '1';
$this->setzeFormularParameter(
@@ -181,17 +214,24 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'values',
$this->automatischeVerbraucherFuerFormular()
);
$lastspitzenOptionen = $this->lastspitzenOptionen($peakShavingLizenziert);
$this->setzeFormularParameter(
$formular['elements'],
'Lastspitzenmodus',
'options',
$lastspitzenOptionen
);
$this->setzeFormularParameter(
$formular['elements'],
'Lastspitzengrenze',
'visible',
$lastspitzenmodus === 1
$peakShavingLizenziert && $lastspitzenmodus === 1
);
$this->setzeFormularParameter(
$formular['elements'],
'MonatsgrenzenUmschalten',
'visible',
$lastspitzenmodus === 2
$peakShavingLizenziert && $lastspitzenmodus === 2
);
$this->setzeFormularParameter(
$formular['elements'],
@@ -242,6 +282,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
$this->aktualisiereLizenzfreigabe($wert, true);
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
$this->aktualisiereLastspitzenformular();
return;
case 'FormAutomatischeSuche':
@@ -277,6 +318,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
case 'FormLastspitzenmodus':
$modus = (int) $wert;
if ($modus !== 0 && !$this->peakShavingLizenziert()) {
$this->UpdateFormField('Lastspitzenmodus', 'value', 0);
$this->UpdateFormField('Lastspitzengrenze', 'visible', false);
$this->UpdateFormField('MonatsgrenzenUmschalten', 'visible', false);
throw new InvalidArgumentException(
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.'
);
}
$this->UpdateFormField('Lastspitzengrenze', 'visible', $modus === 1);
$this->UpdateFormField('MonatsgrenzenUmschalten', 'visible', $modus === 2);
if ($modus !== 2) {
@@ -346,6 +395,17 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->setzeNichtGeregelteVerbraucher('Lizenz nicht freigegeben');
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
try {
$this->aktiveVerbraucherIDs();
} catch (Throwable $fehler) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->setzeDiagnosewert('Lizenzstatus', 'Nicht freigegeben: ' . $fehler->getMessage());
$this->setzeNichtGeregelteVerbraucher($fehler->getMessage());
$this->SetStatus(self::STATUS_LIZENZ_UNGUELTIG);
return;
}
@@ -358,6 +418,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (!$aktiv || !$netzleistungGueltig) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->setzeNichtGeregelteVerbraucher(
$aktiv ? 'Netzleistungsmessung fehlt oder ist veraltet' : 'Manager ist deaktiviert'
);
$this->SetStatus($aktiv ? self::STATUS_MESSWERT_UNGUELTIG : self::STATUS_AKTIV);
return;
}
@@ -385,8 +448,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
$stoerungen = [];
$nichtGeregelt = [];
$synchronisiert = true;
$verbraucher = $this->aktuelleVerbraucher($stoerungen, $betriebsart, $synchronisiert);
$verbraucher = $this->aktuelleVerbraucher(
$stoerungen,
$nichtGeregelt,
$betriebsart,
$synchronisiert
);
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
if (!$synchronisiert) {
$this->setzeDiagnosewert('Verteilbudget', 0.0);
@@ -394,7 +463,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'Betriebsart' => $betriebsart,
'Synchronisiert' => false,
'Verbraucher' => $verbraucher,
'NichtGeregelt' => $nichtGeregelt,
], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert(
'NichtGeregelteGeraete',
$this->nichtGeregelteGeraeteText($nichtGeregelt)
);
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
$this->SetStatus(self::STATUS_AKTIV);
return;
@@ -415,7 +489,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'Synchronisiert' => true,
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
'Verbraucher' => $verbraucher,
'NichtGeregelt' => [],
], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert(
'NichtGeregelteGeraete',
'Alle aktiven Verbraucher werden geregelt.'
);
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
foreach ($ergebnis['Sollwerte'] as $instanzID => $sollleistung) {
@@ -457,10 +536,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
/**
* @param list<string> $stoerungen
* @param list<array<string, mixed>> $nichtGeregelt
* @return list<array{InstanzID: int, Daten: array<string, mixed>}>
*/
private function aktuelleVerbraucher(
array &$stoerungen,
array &$nichtGeregelt,
string $betriebsart,
bool &$synchronisiert
): array {
@@ -483,11 +564,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if ($grund !== '') {
$synchronisiert = false;
$stoerungen[] = $grund . ' von Instanz ' . $instanzID . '.';
$detail = $this->verbraucherDetail($instanzID, $grund, $eintrag);
$nichtGeregelt[] = $detail;
$stoerungen[] = $detail['Name'] . ': ' . $grund . '.';
try {
$this->sendeManagerdaten($instanzID, $betriebsart, null);
} catch (Throwable $fehler) {
$stoerungen[] = 'Betriebsart an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: '
. $fehler->getMessage();
}
continue;
}
@@ -500,8 +584,124 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return $aktuell;
}
private function setzeNichtGeregelteVerbraucher(string $grund): void
{
try {
$ids = $this->konfigurierteAktiveVerbraucherIDs();
} catch (Throwable $fehler) {
$ids = [];
$grund .= '; Verbraucherzuordnung ungueltig: ' . $fehler->getMessage();
}
$cache = $this->leseJsonAttribut('VerbraucherCache');
$nichtGeregelt = [];
$stoerungen = [];
foreach ($ids as $instanzID) {
$detail = $this->verbraucherDetail(
$instanzID,
$grund,
$cache[(string) $instanzID] ?? null
);
$nichtGeregelt[] = $detail;
$stoerungen[] = $detail['Name'] . ': ' . $grund . '.';
}
$this->setzeDiagnosewert('VerbraucherAnzahl', count($ids));
$this->setzeDiagnosewert('VerbraucherVerfuegbar', 0);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'Betriebsart' => ManagerRegler::BETRIEBSART_INAKTIV,
'Synchronisiert' => false,
'Verbraucher' => [],
'NichtGeregelt' => $nichtGeregelt,
], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert(
'NichtGeregelteGeraete',
$this->nichtGeregelteGeraeteText($nichtGeregelt)
);
$this->aktualisiereStoerungen([], $stoerungen);
}
/**
* @param array<string, mixed>|null $cacheEintrag
* @return array<string, mixed>
*/
private function verbraucherDetail(
int $instanzID,
string $grund,
?array $cacheEintrag
): array {
$name = 'Instanz ' . $instanzID;
$typ = 'Unbekannter Verbraucher';
$instanzStatus = null;
if (IPS_InstanceExists($instanzID)) {
$name = IPS_GetName($instanzID);
$instanz = IPS_GetInstance($instanzID);
$instanzStatus = (int) ($instanz['InstanceStatus'] ?? 0);
$modulID = strtoupper((string) ($instanz['ModuleInfo']['ModuleID'] ?? ''));
$typ = self::VERBRAUCHER_LIZENZEN[$modulID]['Name'] ?? $typ;
}
$daten = is_array($cacheEintrag['Daten'] ?? null)
? $cacheEintrag['Daten']
: [];
$empfangenAm = is_int($cacheEintrag['EmpfangenAm'] ?? null)
? $cacheEintrag['EmpfangenAm']
: null;
return [
'InstanzID' => $instanzID,
'Name' => $name,
'Typ' => $typ,
'Grund' => $grund,
'InstanzStatus' => $instanzStatus,
'LetzteMeldung' => $empfangenAm !== null ? date(DATE_ATOM, $empfangenAm) : null,
'GemeldeteBetriebsart' => is_string($daten['Betriebsart'] ?? null)
? $daten['Betriebsart']
: null,
'Verfuegbar' => is_bool($daten['Verfuegbar'] ?? null)
? $daten['Verfuegbar']
: null,
];
}
/**
* @param list<array<string, mixed>> $nichtGeregelt
*/
private function nichtGeregelteGeraeteText(array $nichtGeregelt): string
{
if ($nichtGeregelt === []) {
return 'Alle aktiven Verbraucher werden geregelt.';
}
$zeilen = [];
foreach ($nichtGeregelt as $detail) {
$teile = [
(string) ($detail['Name'] ?? 'Unbekanntes Geraet'),
(string) ($detail['Typ'] ?? 'Unbekannter Typ'),
(string) ($detail['Grund'] ?? 'Grund unbekannt'),
];
if (is_string($detail['LetzteMeldung'] ?? null)) {
$teile[] = 'letzte Meldung: ' . $detail['LetzteMeldung'];
}
if (is_bool($detail['Verfuegbar'] ?? null)) {
$teile[] = 'verfuegbar: ' . ($detail['Verfuegbar'] ? 'ja' : 'nein');
}
$zeilen[] = implode(' | ', $teile);
}
return implode("\n", $zeilen);
}
/** @return list<int> */
private function aktiveVerbraucherIDs(): array
{
$ids = $this->konfigurierteAktiveVerbraucherIDs();
$this->pruefeVerbraucherlizenzen($ids);
return $ids;
}
/** @return list<int> */
private function konfigurierteAktiveVerbraucherIDs(): array
{
if (!$this->ReadPropertyBoolean('AutomatischeSuche')) {
return $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
@@ -521,6 +721,64 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return $this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
}
/**
* @param list<int> $ids
*/
private function pruefeVerbraucherlizenzen(array $ids): void
{
[$mengen, $namen] = $this->verbraucherLizenzbelegung($ids);
try {
Lizenzpruefung::pruefeVerbrauchermengen($this->leseLizenzLease(), $mengen);
} catch (InvalidArgumentException $fehler) {
foreach ($mengen as $kennung => $verwendet) {
$lizenziert = Lizenzpruefung::berechtigungsMenge(
$this->leseLizenzLease(),
$kennung
);
if ($verwendet > $lizenziert) {
throw new InvalidArgumentException(sprintf(
'%s: %d aktiviert, aber nur %d lizenziert. Bitte Auswahl reduzieren oder Lizenz erweitern.',
$namen[$kennung],
$verwendet,
$lizenziert
), 0, $fehler);
}
}
throw $fehler;
}
}
/**
* @param list<int> $ids
* @return array{0: array<string, int>, 1: array<string, string>}
*/
private function verbraucherLizenzbelegung(array $ids): array
{
$mengen = [];
$namen = [];
foreach ($ids as $instanzID) {
if (!IPS_InstanceExists($instanzID)) {
throw new InvalidArgumentException(
'Die Verbraucherinstanz ' . $instanzID . ' existiert nicht.'
);
}
$instanz = IPS_GetInstance($instanzID);
$modulID = strtoupper((string) ($instanz['ModuleInfo']['ModuleID'] ?? ''));
$lizenz = self::VERBRAUCHER_LIZENZEN[$modulID] ?? null;
if (!is_array($lizenz)) {
throw new InvalidArgumentException(
'Instanz ' . $instanzID . ' ist kein lizenzierbarer Enelix-Verbraucher.'
);
}
$kennung = $lizenz['Katalog'];
$mengen[$kennung] = ($mengen[$kennung] ?? 0) + 1;
$namen[$kennung] = $lizenz['Name'];
}
return [$mengen, $namen];
}
/** @return list<int> */
private function leseAktiveVerbraucherIDs(string $property): array
{
@@ -579,7 +837,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$suchbereichID ??= $this->ReadPropertyInteger('SuchbereichID');
$werte = [];
foreach (self::VERBRAUCHER_MODUL_IDS as $modulID) {
foreach (array_keys(self::VERBRAUCHER_LIZENZEN) as $modulID) {
foreach (IPS_GetInstanceListByModuleID($modulID) as $instanzID) {
if (!$this->istImSuchbereich($instanzID, $suchbereichID)) {
continue;
@@ -743,6 +1001,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
80
);
$this->RegisterVariableString('Verbraucherstatus', 'Verbraucherstatus', '', 90);
$this->RegisterVariableString(
'NichtGeregelteGeraete',
'Nicht geregelte Geraete',
'',
95
);
$this->RegisterVariableString('Monatsgrenzen', 'Monatsgrenzen', '', 100);
$this->RegisterVariableString('Prognosestatus', 'Prognosestatus', '', 110);
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
@@ -796,23 +1060,59 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
$lease = $this->leseLizenzLease();
$codePasst = $gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash);
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
$basisGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
$lease,
$installationID,
0
);
$gueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
$lease,
$installationID,
$this->ReadPropertyInteger('Lastspitzenmodus')
$lastspitzenmodus
);
$faellig = !$gueltig || Lizenzpruefung::erneuerungFaellig($lease);
$berechtigungenVeraltet = $basisGueltig
&& !$this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|| Lizenzpruefung::erneuerungFaellig($lease);
$jetzt = time();
if (!$erzwingen
&& (!$faellig || $jetzt < $this->ReadAttributeInteger('LizenzNaechsterVersuch'))
) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
if (!$erzwingen) {
if (!$routineFaellig && !$berechtigungenVeraltet) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
return $gueltig;
return $gueltig;
}
if ($routineFaellig
&& $jetzt < $this->ReadAttributeInteger('LizenzNaechsterVersuch')
) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
return $gueltig;
}
if ($berechtigungenVeraltet
&& $jetzt < $this->ReadAttributeInteger(
'LizenzBerechtigungenNaechsterVersuch'
)
) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
return $gueltig;
}
}
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $jetzt + self::LIZENZ_RETRY_SEKUNDEN);
if ($routineFaellig || $erzwingen) {
$this->WriteAttributeInteger(
'LizenzNaechsterVersuch',
$jetzt + self::LIZENZ_RETRY_SEKUNDEN
);
}
if ($berechtigungenVeraltet || $erzwingen) {
$this->WriteAttributeInteger(
'LizenzBerechtigungenNaechsterVersuch',
$jetzt + self::LIZENZ_RETRY_SEKUNDEN
);
}
try {
$neueLease = $this->rufeLizenzserverAuf($normalisiert, $installationID);
Lizenzpruefung::pruefeLease(
@@ -825,9 +1125,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
);
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
$naechsteErneuerung = Lizenzpruefung::erneuernAb($neueLease);
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $naechsteErneuerung);
$this->WriteAttributeInteger(
'LizenzNaechsterVersuch',
Lizenzpruefung::erneuernAb($neueLease)
'LizenzBerechtigungenNaechsterVersuch',
$naechsteErneuerung
);
$this->WriteAttributeString('LizenzLetzterFehler', '');
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
@@ -860,6 +1162,108 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
}
private function peakShavingLizenziert(): bool
{
$lizenzcode = Lizenzpruefung::normalisiereCode(
$this->ReadPropertyString('Lizenzcode')
);
if (!Lizenzpruefung::istLizenzcodeGueltig($lizenzcode)) {
return false;
}
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
if ($gespeicherterHash === ''
|| !hash_equals($gespeicherterHash, hash('sha256', $lizenzcode))
) {
return false;
}
$lease = $this->leseLizenzLease();
if (!Lizenzpruefung::istLeaseGueltig(
$lease,
$this->ReadAttributeString('LizenzInstallationID'),
0
)) {
return false;
}
return Lizenzpruefung::berechtigungsMenge(
$lease,
Lizenzpruefung::MANAGER_PEAK
) > 0;
}
/**
* @param array<string, mixed> $lease
*/
private function lizenzBerechtigungenPassen(array $lease, int $lastspitzenmodus): bool
{
if ($lastspitzenmodus !== 0
&& Lizenzpruefung::berechtigungsMenge(
$lease,
Lizenzpruefung::MANAGER_PEAK
) < 1
) {
return false;
}
try {
[$mengen] = $this->verbraucherLizenzbelegung(
$this->konfigurierteAktiveVerbraucherIDs()
);
Lizenzpruefung::pruefeVerbrauchermengen($lease, $mengen);
return true;
} catch (Throwable $fehler) {
return false;
}
}
/**
* @return list<array{caption: string, value: int}>
*/
private function lastspitzenOptionen(bool $lizenziert): array
{
$optionen = [[
'caption' => $lizenziert
? 'Aus (Peak-Shaving-Lizenz verfuegbar)'
: 'Aus (Peak-Shaving-Lizenz fehlt)',
'value' => 0,
]];
if ($lizenziert) {
$optionen[] = ['caption' => 'Konstant', 'value' => 1];
$optionen[] = ['caption' => 'Monatlich', 'value' => 2];
}
return $optionen;
}
private function aktualisiereLastspitzenformular(): void
{
$lizenziert = $this->peakShavingLizenziert();
$modus = $lizenziert ? $this->ReadPropertyInteger('Lastspitzenmodus') : 0;
$this->UpdateFormField(
'Lastspitzenmodus',
'options',
json_encode($this->lastspitzenOptionen($lizenziert), JSON_THROW_ON_ERROR)
);
$this->UpdateFormField('Lastspitzenmodus', 'value', $modus);
$this->UpdateFormField(
'Lastspitzengrenze',
'visible',
$lizenziert && $modus === 1
);
$this->UpdateFormField(
'MonatsgrenzenUmschalten',
'visible',
$lizenziert && $modus === 2
);
if (!$lizenziert || $modus !== 2) {
$this->SetBuffer('MonatsgrenzenSichtbar', '0');
$this->UpdateFormField('Monatsgrenzen', 'visible', false);
}
}
private function lizenzStatustext(?string $lizenzcode = null): string
{
$code = Lizenzpruefung::normalisiereCode(
@@ -886,12 +1290,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->ReadAttributeString('LizenzInstallationID'),
$this->ReadPropertyInteger('Lastspitzenmodus')
);
$this->aktiveVerbraucherIDs();
$status = ($lease['development'] ?? false) === true
? 'Aktiv (Entwicklung)'
: 'Aktiv';
$status .= ' bis ' . date('d.m.Y H:i', Lizenzpruefung::offlineBis($lease));
? 'Aktiv (Entwicklung) - unbeschraenkt gueltig.'
: 'Aktiv - unbeschraenkt gueltig.';
if ($fehler !== '') {
$status .= '; Erneuerung fehlgeschlagen: ' . $fehler;
$status .= ' Online-Pruefung fehlgeschlagen; lokale Freigabe endet am '
. date('d.m.Y H:i', Lizenzpruefung::offlineBis($lease))
. ', sofern keine Verbindung hergestellt wird. '
. 'Die gekaufte Lizenz bleibt unbeschraenkt gueltig. Fehler: '
. $fehler;
}
return $status;
+56
View File
@@ -82,6 +82,62 @@ final class Lizenzpruefung
}
}
/**
* Liefert die aufsummierte aktive Berechtigung fuer einen Katalogeintrag.
*
* @param array<string, mixed> $lease
*/
public static function berechtigungsMenge(array $lease, string $kennung): int
{
$berechtigungen = $lease['entitlements'] ?? null;
if (!is_array($berechtigungen)) {
return 0;
}
$menge = 0;
foreach ($berechtigungen as $berechtigung) {
if (!is_array($berechtigung)
|| ($berechtigung['status'] ?? null) !== 'active'
|| !is_int($berechtigung['quantity'] ?? null)
|| $berechtigung['quantity'] < 1
) {
continue;
}
$berechtigungsKennung = $berechtigung['catalog_key']
?? $berechtigung['sku']
?? null;
if ($berechtigungsKennung === $kennung) {
$menge += $berechtigung['quantity'];
}
}
return $menge;
}
/**
* @param array<string, mixed> $lease
* @param array<string, int> $verwendeteMengen
*/
public static function pruefeVerbrauchermengen(array $lease, array $verwendeteMengen): void
{
foreach ($verwendeteMengen as $kennung => $verwendet) {
if ($verwendet < 0) {
throw new InvalidArgumentException(
'Die verwendete Menge fuer ' . $kennung . ' darf nicht negativ sein.'
);
}
$lizenziert = self::berechtigungsMenge($lease, $kennung);
if ($verwendet > $lizenziert) {
throw new InvalidArgumentException(sprintf(
'Lizenzkontingent fuer %s ueberschritten: %d verwendet, %d lizenziert.',
$kennung,
$verwendet,
$lizenziert
));
}
}
}
/**
* @param array<string, mixed> $lease
*/
+69
View File
@@ -104,6 +104,75 @@ final class LizenzpruefungTest extends TestCase
);
}
public function testZweiLizenzierteBoilerWerdenFreigegeben(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'][] = [
'sku' => 'ENX-BOILER-MULTI',
'catalog_key' => 'boiler_multi',
'quantity' => 2,
'status' => 'active',
];
self::assertSame(2, Lizenzpruefung::berechtigungsMenge($lease, 'boiler_multi'));
Lizenzpruefung::pruefeVerbrauchermengen($lease, ['boiler_multi' => 2]);
self::assertTrue(true);
}
public function testDritterBoilerBeiZweiLizenzenWirdAbgelehnt(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'][] = [
'sku' => 'ENX-BOILER-MULTI',
'catalog_key' => 'boiler_multi',
'quantity' => 2,
'status' => 'active',
];
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'Lizenzkontingent fuer boiler_multi ueberschritten: 3 verwendet, 2 lizenziert.'
);
Lizenzpruefung::pruefeVerbrauchermengen($lease, ['boiler_multi' => 3]);
}
public function testVerbraucherkontingenteWerdenJeTypSummiertUndGetrenntGeprueft(): void
{
$lease = $this->lease('manager_standard');
$lease['entitlements'] = array_merge($lease['entitlements'], [
[
'catalog_key' => 'boiler_multi',
'quantity' => 1,
'status' => 'active',
],
[
'catalog_key' => 'boiler_multi',
'quantity' => 1,
'status' => 'active',
],
[
'catalog_key' => 'battery',
'quantity' => 1,
'status' => 'active',
],
[
'catalog_key' => 'battery',
'quantity' => 10,
'status' => 'revoked',
],
]);
self::assertSame(2, Lizenzpruefung::berechtigungsMenge($lease, 'boiler_multi'));
self::assertSame(1, Lizenzpruefung::berechtigungsMenge($lease, 'battery'));
Lizenzpruefung::pruefeVerbrauchermengen($lease, [
'boiler_multi' => 2,
'battery' => 1,
]);
self::assertTrue(true);
}
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
{
self::assertSame(
+41
View File
@@ -52,6 +52,7 @@ final class ManagerModulstrukturTest extends TestCase
'LoggingEin',
'Lizenzcode',
'LizenzPruefen',
'LizenzVerwalten',
'LizenzInstallationID',
'LizenzInformation',
'VerbraucherAktualisieren',
@@ -69,6 +70,8 @@ final class ManagerModulstrukturTest extends TestCase
self::assertStringContainsString($bereich, $json);
}
self::assertSame('Lizenzierung', $formular['elements'][0]['caption']);
self::assertStringContainsString('https://license.enelix.ch', $json);
self::assertStringContainsString('"link":true', $json);
self::assertStringContainsString(
"'SuchbereichID' => \$SuchbereichID",
$json
@@ -111,6 +114,44 @@ final class ManagerModulstrukturTest extends TestCase
self::assertStringContainsString("RegisterAttributeString('LizenzcodeHash'", $inhalt);
self::assertStringContainsString('STATUS_LIZENZ_UNGUELTIG', $inhalt);
self::assertStringContainsString('aktualisiereLizenzfreigabe(', $inhalt);
self::assertStringContainsString('pruefeVerbraucherlizenzen(', $inhalt);
self::assertStringContainsString('lizenzBerechtigungenPassen(', $inhalt);
self::assertStringContainsString('peakShavingLizenziert(', $inhalt);
self::assertStringContainsString('Aus (Peak-Shaving-Lizenz fehlt)', $inhalt);
self::assertStringContainsString('Aus (Peak-Shaving-Lizenz verfuegbar)', $inhalt);
self::assertStringContainsString(
'Konstant und Monatlich benoetigen eine Peak-Shaving-Lizenz.',
$inhalt
);
foreach (['battery', 'buffer_storage', 'ev_charger', 'consumer_single', 'boiler_multi'] as $katalog) {
self::assertStringContainsString("'Katalog' => '" . $katalog . "'", $inhalt);
}
foreach ([
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}',
'{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}',
'{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}',
] as $modulID) {
self::assertStringContainsString("'" . $modulID . "' =>", $inhalt);
}
self::assertStringContainsString("'Name' => 'Pufferspeicher'", $inhalt);
self::assertStringContainsString("'Name' => 'Batteriespeicher'", $inhalt);
self::assertStringContainsString(
'Bitte Auswahl reduzieren oder Lizenz erweitern.',
$inhalt
);
foreach ([
"'NichtGeregelt'",
"'NichtGeregelteGeraete'",
"'InstanzStatus'",
"'LetzteMeldung'",
"'GemeldeteBetriebsart'",
"'Verfuegbar'",
] as $detailfeld) {
self::assertStringContainsString($detailfeld, $inhalt);
}
self::assertStringNotContainsString("'publicKey' =>", $inhalt);
}
+16 -1
View File
@@ -163,7 +163,7 @@ return static function (TestContext $test): void {
if ($lizenzcode === '') {
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
TestContext $test
) use ($managerId): void {
) use ($managerId, $verbraucherstatus): void {
IPS_RequestAction($managerId, 'Aktiv', true);
IPS_RequestAction($managerId, 'Regeln', true);
$test->assertInstanceStatus($managerId, 203);
@@ -171,6 +171,21 @@ return static function (TestContext $test): void {
'Inaktiv',
GetValue($test->objectByIdent('Betriebsart', $managerId))
);
$daten = $verbraucherstatus();
$test->assertSame(false, $daten['Synchronisiert'] ?? null);
$test->assertSame(3, count($daten['NichtGeregelt'] ?? []));
foreach ($daten['NichtGeregelt'] as $detail) {
$test->assertTrue(is_int($detail['InstanzID'] ?? null));
$test->assertTrue(is_string($detail['Name'] ?? null));
$test->assertTrue(is_string($detail['Typ'] ?? null));
$test->assertSame('Lizenz nicht freigegeben', $detail['Grund'] ?? null);
}
$detailtext = (string) GetValue(
$test->objectByIdent('NichtGeregelteGeraete', $managerId)
);
$test->assertTrue(str_contains($detailtext, 'Boiler'));
$test->assertTrue(str_contains($detailtext, 'Lizenz nicht freigegeben'));
});
return;