Files
Enelix-EMS/Manager/module.php
T

1490 lines
57 KiB
PHP

<?php
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;
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_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',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
];
private const DIAGNOSE_VARIABLEN = [
'NetzleistungGueltig',
'WirksameLastspitzengrenze',
'Verteilbudget',
'VerbraucherAnzahl',
'VerbraucherVerfuegbar',
'Verbraucherstatus',
'NichtGeregelteGeraete',
'Monatsgrenzen',
'Prognosestatus',
'SDLStatus',
'Lizenzstatus',
'Stoerueberwachungsstatus',
'Sammelstoerung',
'Stoertext',
];
public function Create(): void
{
parent::Create();
$this->RegisterPropertyInteger('Rolle', 0);
$this->RegisterPropertyInteger('NetzleistungVariableID', 0);
$this->RegisterPropertyFloat('Netzleistungsfaktor', 1.0);
$this->RegisterPropertyInteger('MesswertMaxAlter', 60);
$this->RegisterPropertyString('VerbraucherZuordnung', '[]');
$this->RegisterPropertyString('AutomatischeVerbraucherZuordnung', '[]');
$this->RegisterPropertyBoolean('AutomatischeSuche', true);
$this->RegisterPropertyInteger('SuchbereichID', 0);
$this->RegisterPropertyInteger('KeepAlive', 60);
$this->RegisterPropertyInteger('VerbraucherTimeout', 60);
$this->RegisterPropertyInteger('Lastspitzenmodus', 0);
$this->RegisterPropertyFloat('Lastspitzengrenze', 0.0);
$this->RegisterPropertyString('Monatsgrenzen', $this->standardMonatsgrenzen());
$this->RegisterPropertyFloat('SollwertSolarladen', 0.0);
$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);
$this->RegisterPropertyBoolean('LoggingEin', false);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv');
$this->RegisterVariableString('Betriebsart', 'Betriebsart', '', 20);
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
$this->RegisterAttributeString('VerbraucherCache', '{}');
$this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeInteger('RegistrierteNetzleistungVariableID', 0);
$this->RegisterAttributeString('LizenzInstallationID', '');
$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());
}
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, '');
$this->RegisterTimer('VorgabenErneuern', 0, '');
$this->RegisterTimer(
'KeepAlive',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
);
}
public function ApplyChanges(): void
{
parent::ApplyChanges();
$this->aktualisiereVariablen();
try {
$this->pruefeKonfiguration();
} catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', 0);
$this->protokolliere('Konfiguration', $fehler->getMessage());
return;
}
$this->registriereNetzleistungsmeldung();
$this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0);
$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;
}
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);
}
public function GetConfigurationForm(): string
{
$formular = json_decode(
(string) file_get_contents(__DIR__ . '/form.json'),
true,
512,
JSON_THROW_ON_ERROR
);
$automatisch = $this->ReadPropertyBoolean('AutomatischeSuche');
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
$peakShavingLizenziert = $this->peakShavingLizenziert();
$monatsgrenzenSichtbar = $peakShavingLizenziert
&& $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'],
'AutomatischeVerbraucherZuordnung',
'visible',
$automatisch
);
$this->setzeFormularParameter($formular['elements'], 'SuchbereichID', 'visible', $automatisch);
$this->setzeFormularParameter(
$formular['elements'],
'VerbraucherAktualisieren',
'visible',
$automatisch
);
$this->setzeFormularParameter(
$formular['elements'],
'AutomatischeVerbraucherZuordnung',
'values',
$this->automatischeVerbraucherFuerFormular()
);
$lastspitzenOptionen = $this->lastspitzenOptionen($peakShavingLizenziert);
$this->setzeFormularParameter(
$formular['elements'],
'Lastspitzenmodus',
'options',
$lastspitzenOptionen
);
$this->setzeFormularParameter(
$formular['elements'],
'Lastspitzengrenze',
'visible',
$peakShavingLizenziert && $lastspitzenmodus === 1
);
$this->setzeFormularParameter(
$formular['elements'],
'MonatsgrenzenUmschalten',
'visible',
$peakShavingLizenziert && $lastspitzenmodus === 2
);
$this->setzeFormularParameter(
$formular['elements'],
'MonatsgrenzenUmschalten',
'caption',
$monatsgrenzenSichtbar ? 'Monatsgrenzen ausblenden' : 'Monatsgrenzen anzeigen'
);
$this->setzeFormularParameter(
$formular['elements'],
'Monatsgrenzen',
'visible',
$monatsgrenzenSichtbar
);
$this->setzeFormularParameter(
$formular['elements'],
'Monatsgrenzen',
'values',
$this->monatsgrenzenFuerFormular()
);
return json_encode($formular, JSON_THROW_ON_ERROR);
}
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
{
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
&& (int) $nachricht === self::VM_UPDATE
) {
$this->regeln(false);
}
}
public function RequestAction($ident, $wert): void
{
switch ($ident) {
case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert);
$this->regeln(false);
return;
case 'Regeln':
$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));
$this->aktualisiereLastspitzenformular();
return;
case 'FormAutomatischeSuche':
$automatisch = (bool) $wert;
$this->UpdateFormField('VerbraucherZuordnung', 'visible', !$automatisch);
$this->UpdateFormField('AutomatischeVerbraucherZuordnung', 'visible', $automatisch);
$this->UpdateFormField('SuchbereichID', 'visible', $automatisch);
$this->UpdateFormField('VerbraucherAktualisieren', 'visible', $automatisch);
return;
case 'FormVerbraucherAktualisieren':
if (!is_string($wert)) {
throw new InvalidArgumentException('Suchparameter muessen als JSON uebergeben werden.');
}
$suchparameter = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($suchparameter) || !is_array($suchparameter['Zuordnung'] ?? null)) {
throw new InvalidArgumentException('Suchparameter sind ungueltig.');
}
$suchbereichID = $suchparameter['SuchbereichID'] ?? 0;
if (!is_int($suchbereichID)) {
throw new InvalidArgumentException('SuchbereichID muss eine Ganzzahl sein.');
}
$werte = $this->automatischeVerbraucherFuerFormular(
$suchparameter['Zuordnung'],
$suchbereichID
);
$this->UpdateFormField(
'AutomatischeVerbraucherZuordnung',
'values',
json_encode($werte, JSON_THROW_ON_ERROR)
);
return;
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) {
$this->SetBuffer('MonatsgrenzenSichtbar', '0');
$this->UpdateFormField('Monatsgrenzen', 'visible', false);
$this->UpdateFormField(
'MonatsgrenzenUmschalten',
'caption',
'Monatsgrenzen anzeigen'
);
}
return;
case 'FormMonatsgrenzenUmschalten':
$sichtbar = $this->GetBuffer('MonatsgrenzenSichtbar') !== '1';
$this->SetBuffer('MonatsgrenzenSichtbar', $sichtbar ? '1' : '0');
$this->UpdateFormField('Monatsgrenzen', 'visible', $sichtbar);
$this->UpdateFormField(
'MonatsgrenzenUmschalten',
'caption',
$sichtbar ? 'Monatsgrenzen ausblenden' : 'Monatsgrenzen anzeigen'
);
return;
case 'VerbraucherdatenEmpfangen':
if (!is_string($wert)) {
throw new InvalidArgumentException('Verbraucherdaten muessen als JSON uebergeben werden.');
}
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($daten)) {
throw new InvalidArgumentException('Verbraucherdaten muessen ein JSON-Objekt sein.');
}
$this->VerbraucherdatenEmpfangen($daten);
return;
}
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
}
/**
* @param array<string, mixed> $daten
*/
public function VerbraucherdatenEmpfangen(array $daten): void
{
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
throw new InvalidArgumentException('Verbraucherdaten sind an eine andere Instanz adressiert.');
}
$absenderID = $daten['Kopf']['AbsenderID'];
if (!in_array($absenderID, $this->aktiveVerbraucherIDs(), true)) {
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
}
$cache = $this->leseJsonAttribut('VerbraucherCache');
$cache[(string) $absenderID] = [
'EmpfangenAm' => time(),
'Daten' => $daten,
];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->regeln(false);
}
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->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;
}
[$netzleistung, $netzleistungGueltig] = $this->leseNetzleistung();
$this->SetValue('Netzleistung', $netzleistung);
$this->setzeDiagnosewert('NetzleistungGueltig', $netzleistungGueltig);
$aktiv = (bool) $this->GetValue('Aktiv');
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;
}
$lastspitzengrenze = ManagerRegler::wirksameLastspitzengrenze(
$this->ReadPropertyInteger('Lastspitzenmodus'),
$this->ReadPropertyFloat('Lastspitzengrenze'),
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen')),
(int) date('n')
);
$bisherigeBetriebsart = (string) $this->GetValue('Betriebsart');
$betriebsart = ManagerRegler::bestimmeBetriebsart(
true,
true,
$netzleistung,
$bisherigeBetriebsart,
$lastspitzengrenze !== null,
$this->ReadPropertyFloat('SollwertSolarladen'),
$lastspitzengrenze ?? 0.0,
$this->ReadPropertyFloat('Umschaltdifferenz')
);
$this->SetValue('Betriebsart', $betriebsart);
if ($betriebsart !== $bisherigeBetriebsart) {
$this->WriteAttributeString('LetzteSollwerte', '{}');
}
$stoerungen = [];
$nichtGeregelt = [];
$synchronisiert = true;
$verbraucher = $this->aktuelleVerbraucher(
$stoerungen,
$nichtGeregelt,
$betriebsart,
$synchronisiert
);
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
if (!$synchronisiert) {
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'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;
}
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
? (float) $lastspitzengrenze
: $this->ReadPropertyFloat('SollwertSolarladen');
$ergebnis = ManagerRegler::verteile(
$netzleistung,
$zielnetzleistung,
$verbraucher,
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV'
);
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'Betriebsart' => $betriebsart,
'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) {
if (!$erneuern && ($letzteSollwerte[(string) $instanzID] ?? null) === $sollleistung) {
continue;
}
try {
$this->sendeManagerdaten((int) $instanzID, $betriebsart, $sollleistung);
$letzteSollwerte[(string) $instanzID] = $sollleistung;
} catch (Throwable $fehler) {
$stoerungen[] = 'Sollwert an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
}
}
$this->WriteAttributeString('LetzteSollwerte', json_encode($letzteSollwerte, JSON_THROW_ON_ERROR));
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
$this->SetStatus(self::STATUS_AKTIV);
}
/** @return array{0: float, 1: bool} */
private function leseNetzleistung(): array
{
$variablenID = $this->ReadPropertyInteger('NetzleistungVariableID');
if ($variablenID <= 0 || !IPS_VariableExists($variablenID)) {
return [0.0, false];
}
$variable = IPS_GetVariable($variablenID);
if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('MesswertMaxAlter')) {
return [0.0, false];
}
$wert = GetValue($variablenID);
if (!is_int($wert) && !is_float($wert)) {
return [0.0, false];
}
return [(float) $wert * $this->ReadPropertyFloat('Netzleistungsfaktor'), true];
}
/**
* @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 {
$ids = $this->aktiveVerbraucherIDs();
$this->setzeDiagnosewert('VerbraucherAnzahl', count($ids));
$cache = $this->leseJsonAttribut('VerbraucherCache');
$timeout = $this->ReadPropertyInteger('VerbraucherTimeout');
$aktuell = [];
foreach ($ids as $instanzID) {
$eintrag = $cache[(string) $instanzID] ?? null;
$grund = '';
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) {
$grund = 'Noch keine Meldung';
} elseif (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
$grund = 'Meldung ist veraltet';
} elseif (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) {
$grund = 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart;
}
if ($grund !== '') {
$synchronisiert = false;
$detail = $this->verbraucherDetail($instanzID, $grund, $eintrag);
$nichtGeregelt[] = $detail;
$stoerungen[] = $detail['Name'] . ': ' . $grund . '.';
try {
$this->sendeManagerdaten($instanzID, $betriebsart, null);
} catch (Throwable $fehler) {
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: '
. $fehler->getMessage();
}
continue;
}
$aktuell[] = ['InstanzID' => $instanzID, 'Daten' => $eintrag['Daten']];
}
$verfuegbar = array_filter($aktuell, static fn (array $eintrag): bool => $eintrag['Daten']['Verfuegbar']);
$this->setzeDiagnosewert('VerbraucherVerfuegbar', count($verfuegbar));
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');
}
$gespeichert = json_decode(
$this->ReadPropertyString('AutomatischeVerbraucherZuordnung'),
true,
512,
JSON_THROW_ON_ERROR
);
if ($gespeichert === []) {
// Bestehende Installationen behalten ihre Auswahl, bis die neue Liste gespeichert wurde.
return $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
}
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
{
$zuordnung = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($zuordnung)) {
throw new InvalidArgumentException($property . ' muss eine JSON-Liste sein.');
}
return $this->aktiveVerbraucherIDsAusZuordnung($zuordnung);
}
/**
* @param list<array<string, mixed>> $zuordnung
* @return list<int>
*/
private function aktiveVerbraucherIDsAusZuordnung(array $zuordnung): array
{
$ids = [];
foreach ($zuordnung as $eintrag) {
if (!is_array($eintrag)) {
throw new InvalidArgumentException('Jede Verbraucherzuordnung muss ein Objekt sein.');
}
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
if (!is_int($instanzID) || $instanzID <= 0 || !is_bool($eintrag['Aktiv'] ?? null)) {
throw new InvalidArgumentException('Verbraucherzuordnung benoetigt InstanzID und Aktiv.');
}
if ($eintrag['Aktiv']) {
$ids[] = $instanzID;
}
}
return array_values(array_unique($ids));
}
/**
* @param list<array<string, mixed>>|null $aktuelleZuordnung
* @return list<array{Name: string, InstanzID: int, Aktiv: bool}>
*/
private function automatischeVerbraucherFuerFormular(
?array $aktuelleZuordnung = null,
?int $suchbereichID = null
): array {
if ($aktuelleZuordnung === null) {
$gespeichert = json_decode(
$this->ReadPropertyString('AutomatischeVerbraucherZuordnung'),
true,
512,
JSON_THROW_ON_ERROR
);
$ausgewaehlt = $gespeichert === []
? $this->leseAktiveVerbraucherIDs('VerbraucherZuordnung')
: $this->aktiveVerbraucherIDsAusZuordnung($gespeichert);
} else {
$ausgewaehlt = $this->aktiveVerbraucherIDsAusZuordnung($aktuelleZuordnung);
}
$suchbereichID ??= $this->ReadPropertyInteger('SuchbereichID');
$werte = [];
foreach (array_keys(self::VERBRAUCHER_LIZENZEN) as $modulID) {
foreach (IPS_GetInstanceListByModuleID($modulID) as $instanzID) {
if (!$this->istImSuchbereich($instanzID, $suchbereichID)) {
continue;
}
$werte[] = [
'Name' => IPS_GetName($instanzID),
'InstanzID' => $instanzID,
'Aktiv' => in_array($instanzID, $ausgewaehlt, true),
];
}
}
usort($werte, static function (array $links, array $rechts): int {
$nameVergleich = strcasecmp($links['Name'], $rechts['Name']);
return $nameVergleich !== 0
? $nameVergleich
: $links['InstanzID'] <=> $rechts['InstanzID'];
});
return $werte;
}
private function istImSuchbereich(int $objektID, int $suchbereichID): bool
{
if ($suchbereichID <= 0) {
return true;
}
while ($objektID > 0) {
if ($objektID === $suchbereichID) {
return true;
}
$objektID = IPS_GetParent($objektID);
}
return false;
}
/** @return list<array{Monat: string, Grenze_W: float, MonatIndex: int}> */
private function monatsgrenzenFuerFormular(): array
{
$grenzen = $this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
$werte = [];
foreach (self::MONATSNAMEN as $monat => $name) {
$werte[] = [
'Monat' => $name,
'Grenze_W' => $grenzen[$monat],
'MonatIndex' => $monat,
];
}
return $werte;
}
/** @param list<array<string, mixed>> $elemente */
private function setzeFormularParameter(
array &$elemente,
string $name,
string $parameter,
$wert
): bool {
foreach ($elemente as &$element) {
if (($element['name'] ?? null) === $name) {
$element[$parameter] = $wert;
return true;
}
if (isset($element['items'])
&& is_array($element['items'])
&& $this->setzeFormularParameter($element['items'], $name, $parameter, $wert)
) {
return true;
}
}
unset($element);
return false;
}
private function sendeManagerdaten(
int $instanzID,
string $betriebsart,
?int $sollleistung
): void {
if (!IPS_InstanceExists($instanzID)) {
throw new RuntimeException('Instanz existiert nicht.');
}
$paket = [
'Kopf' => [
'Version' => Nachrichtenvertrag::VERSION,
'AbsenderID' => $this->InstanceID,
'EmpfaengerID' => $instanzID,
'Zeitpunkt' => time(),
],
'Betriebsart' => $betriebsart,
'Sollleistung_W' => $sollleistung,
];
Nachrichtenvertrag::pruefeManagerdaten($paket);
IPS_RequestAction($instanzID, 'ManagerdatenEmpfangen', json_encode($paket, JSON_THROW_ON_ERROR));
$this->protokolliere('Managerdaten', [
'InstanzID' => $instanzID,
'Betriebsart' => $betriebsart,
'Sollleistung_W' => $sollleistung,
]);
}
/**
* @param list<array{InstanzID: int, Daten: array<string, mixed>}> $verbraucher
* @param list<string> $stoerungen
*/
private function aktualisiereStoerungen(array $verbraucher, array $stoerungen): void
{
foreach ($verbraucher as $eintrag) {
foreach ($eintrag['Daten']['Zustand'] as $zustand) {
if ($zustand['Art'] === 'Stoerung' && $zustand['Wert'] === true) {
$stoerungen[] = 'Instanz ' . $eintrag['InstanzID'] . ': '
. ($zustand['Text'] ?? $zustand['Kennung']);
}
}
}
$stoerungen = array_values(array_unique($stoerungen));
$this->setzeDiagnosewert('Sammelstoerung', $stoerungen !== []);
$this->setzeDiagnosewert('Stoertext', implode("\n", $stoerungen));
}
private function registriereNetzleistungsmeldung(): void
{
$alt = $this->ReadAttributeInteger('RegistrierteNetzleistungVariableID');
$neu = $this->ReadPropertyInteger('NetzleistungVariableID');
if ($alt > 0 && $alt !== $neu) {
$this->UnregisterMessage($alt, self::VM_UPDATE);
}
if ($neu > 0 && $neu !== $alt) {
$this->RegisterMessage($neu, self::VM_UPDATE);
}
$this->WriteAttributeInteger('RegistrierteNetzleistungVariableID', $neu);
}
private function aktualisiereVariablen(): void
{
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean(
'NetzleistungGueltig',
'Netzleistung gueltig',
'~Switch',
40
);
$this->RegisterVariableFloat(
'WirksameLastspitzengrenze',
'Wirksame Lastspitzengrenze',
'',
50
);
$this->RegisterVariableFloat('Verteilbudget', 'Verteilbudget', '', 60);
$this->RegisterVariableInteger('VerbraucherAnzahl', 'Verbraucher Anzahl', '', 70);
$this->RegisterVariableInteger(
'VerbraucherVerfuegbar',
'Verbraucher verfuegbar',
'',
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);
$this->RegisterVariableString('Lizenzstatus', 'Lizenzstatus', '', 130);
$this->RegisterVariableString(
'Stoerueberwachungsstatus',
'Stoerueberwachungsstatus',
'',
140
);
$this->RegisterVariableBoolean('Sammelstoerung', 'Sammelstoerung', '~Alert', 150);
$this->RegisterVariableString('Stoertext', 'Stoertext', '', 160);
return;
}
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
$this->UnregisterVariable($ident);
}
}
}
/** @param bool|float|int|string $wert */
private function setzeDiagnosewert(string $ident, $wert): void
{
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
$this->SetValue($ident, $wert);
}
}
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);
$lastspitzenmodus = $this->ReadPropertyInteger('Lastspitzenmodus');
$basisGueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
$lease,
$installationID,
0
);
$gueltig = $codePasst && Lizenzpruefung::istLeaseGueltig(
$lease,
$installationID,
$lastspitzenmodus
);
$berechtigungenVeraltet = $basisGueltig
&& !$this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|| Lizenzpruefung::erneuerungFaellig($lease);
$jetzt = time();
if (!$erzwingen) {
if (!$routineFaellig && !$berechtigungenVeraltet) {
$this->setzeDiagnosewert('Lizenzstatus', $this->lizenzStatustext($lizenzcode));
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;
}
}
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(
$neueLease,
$installationID,
$this->ReadPropertyInteger('Lastspitzenmodus')
);
$this->WriteAttributeString(
'LizenzLease',
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
);
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
$naechsteErneuerung = Lizenzpruefung::erneuernAb($neueLease);
$this->WriteAttributeInteger('LizenzNaechsterVersuch', $naechsteErneuerung);
$this->WriteAttributeInteger(
'LizenzBerechtigungenNaechsterVersuch',
$naechsteErneuerung
);
$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 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(
$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')
);
$this->aktiveVerbraucherIDs();
$status = ($lease['development'] ?? false) === true
? 'Aktiv (Entwicklung) - unbeschraenkt gueltig.'
: 'Aktiv - unbeschraenkt gueltig.';
if ($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;
} 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',
'StoermeldeAnschluss' => 'Stoerueberwachungsstatus',
] as $property => $variable) {
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
$aktiv = is_array($konfiguration) && ($konfiguration['Aktiv'] ?? false) === true;
$this->setzeDiagnosewert($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
}
}
private function pruefeKonfiguration(): void
{
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
}
}
if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0
|| $this->ReadPropertyFloat('Umschaltdifferenz') > 100
) {
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 liegen.');
}
$this->leseAktiveVerbraucherIDs('VerbraucherZuordnung');
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
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.');
}
}
}
/** @return array<int, float> */
private function dekodiereMonatsgrenzen(string $json): array
{
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($werte)) {
throw new InvalidArgumentException('Monatsgrenzen muss ein JSON-Objekt sein.');
}
$ergebnis = [];
if (isset($werte[0]) && is_array($werte[0])) {
foreach ($werte as $eintrag) {
if (!is_array($eintrag)) {
throw new InvalidArgumentException('Monatsgrenzen muss eine Liste von Monatswerten sein.');
}
$monat = $eintrag['MonatIndex'] ?? null;
$wert = $eintrag['Grenze_W'] ?? null;
if (!is_int($monat) || $monat < 1 || $monat > 12
|| (!is_int($wert) && !is_float($wert))
) {
throw new InvalidArgumentException('Jeder Monatswert benoetigt Monatsnummer und Grenze.');
}
$ergebnis[$monat] = (float) $wert;
}
} else {
for ($monat = 1; $monat <= 12; ++$monat) {
$wert = $werte[(string) $monat] ?? $werte[$monat] ?? null;
if (!is_int($wert) && !is_float($wert)) {
throw new InvalidArgumentException(
'Monatsgrenzen muss fuer jeden Monat einen Zahlenwert enthalten.'
);
}
$ergebnis[$monat] = (float) $wert;
}
}
if (count($ergebnis) !== 12) {
throw new InvalidArgumentException('Monatsgrenzen muss genau zwoelf Monatswerte enthalten.');
}
ksort($ergebnis);
return $ergebnis;
}
/** @return array<string, mixed> */
private function leseJsonAttribut(string $name): array
{
$wert = json_decode($this->ReadAttributeString($name), true, 512, JSON_THROW_ON_ERROR);
return is_array($wert) ? $wert : [];
}
private function standardMonatsgrenzen(): string
{
$werte = [];
foreach (self::MONATSNAMEN as $monat => $name) {
$werte[] = [
'Monat' => $name,
'Grenze_W' => 0.0,
'MonatIndex' => $monat,
];
}
return json_encode($werte, JSON_THROW_ON_ERROR);
}
/** @param mixed $daten */
private function protokolliere(string $bezeichnung, $daten): void
{
if ($this->ReadPropertyBoolean('LoggingEin')) {
$this->SendDebug($bezeichnung, is_string($daten) ? $daten : json_encode($daten), 0);
}
}
}