This commit is contained in:
+250
-10
@@ -8,13 +8,17 @@ require_once __DIR__ . '/../libs/ManagerRegler.php';
|
||||
require_once __DIR__ . '/../libs/EinspeiseRegler.php';
|
||||
require_once __DIR__ . '/../libs/Lizenzpruefung.php';
|
||||
require_once __DIR__ . '/../libs/Anlagentopologie.php';
|
||||
require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
|
||||
require_once __DIR__ . '/../libs/EnergieMessung.php';
|
||||
|
||||
use Belevo\EnelixEMS\Anlagentopologie;
|
||||
use Belevo\EnelixEMS\EinspeiseRegler;
|
||||
use Belevo\EnelixEMS\EnergieMessung;
|
||||
use Belevo\EnelixEMS\Lizenzpruefung;
|
||||
use Belevo\EnelixEMS\ManagerRegler;
|
||||
use Belevo\EnelixEMS\ManagerSchnittstelle;
|
||||
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||
use Belevo\EnelixEMS\StoerungsSnapshot;
|
||||
|
||||
class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
{
|
||||
@@ -26,8 +30,26 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
private const PROGNOSE_TOPOLOGIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/topology';
|
||||
private const PROGNOSE_TELEMETRIE_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry';
|
||||
private const PROGNOSE_FAHRPLAN_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/prognosis/schedule';
|
||||
private const STOERUNGS_ENDPOINT = 'https://license.enelix.ch/api/v1/installations/%s/faults';
|
||||
private const LIZENZ_RETRY_SEKUNDEN = 3600;
|
||||
private const STOERUNGS_RETRY_MAX_SEKUNDEN = 900;
|
||||
private const VM_UPDATE = 10603;
|
||||
private const ARCHIVE_MODULE_ID = '{43192F0B-135B-4CE7-A0A7-1475603F3060}';
|
||||
private const ENERGY_PIE_MODULE_ID = '{F73B8F8D-5E36-4B23-B379-4632A517A1D6}';
|
||||
private const LEISTUNGS_VARIABLEN = [
|
||||
'PVLeistungArchiv',
|
||||
'HausverbrauchLeistungArchiv',
|
||||
'NetzleistungArchiv',
|
||||
'BatterieleistungArchiv',
|
||||
];
|
||||
private const ENERGIE_VARIABLEN = [
|
||||
'PVEnergie',
|
||||
'Hausenergie',
|
||||
'NetzbezugEnergie',
|
||||
'EinspeisungEnergie',
|
||||
'BatterieLadenEnergie',
|
||||
'BatterieEntladenEnergie',
|
||||
];
|
||||
private const VERBRAUCHER_LIZENZEN = [
|
||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
|
||||
'Katalog' => 'boiler_multi',
|
||||
@@ -117,9 +139,25 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterPropertyString('Lizenzcode', '');
|
||||
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
|
||||
$this->RegisterPropertyString('LizenzAnschluss', '{}');
|
||||
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
|
||||
$this->RegisterPropertyString('StoermeldeAnschluss', '{}');
|
||||
$this->RegisterPropertyBoolean('StoerueberwachungAktiv', false);
|
||||
$this->RegisterPropertyInteger('StoerungsSendeintervall', 300);
|
||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||
$this->RegisterPropertyBoolean('LoggingEin', false);
|
||||
$this->RegisterPropertyBoolean('EnergieaufzeichnungAktiv', true);
|
||||
$this->RegisterPropertyInteger('MessungPVLeistungVariableID', 0);
|
||||
$this->RegisterPropertyInteger('MessungHausverbrauchLeistungVariableID', 0);
|
||||
$this->RegisterPropertyInteger('MessungBatterieleistungVariableID', 0);
|
||||
$this->RegisterPropertyFloat('MessungPVLeistungsfaktor', 1.0);
|
||||
$this->RegisterPropertyFloat('MessungHausverbrauchLeistungsfaktor', 1.0);
|
||||
$this->RegisterPropertyFloat('MessungBatterieleistungsfaktor', 1.0);
|
||||
$this->RegisterPropertyBoolean('MesswerteAnzeigen', false);
|
||||
$this->RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1);
|
||||
$this->RegisterPropertyInteger('LeistungLoeschenMonate', 12);
|
||||
$this->RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5);
|
||||
$this->RegisterPropertyInteger('EnergieVerdichtenMonate', 12);
|
||||
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
|
||||
|
||||
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
|
||||
$this->EnableAction('Aktiv');
|
||||
@@ -138,6 +176,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterAttributeInteger('LizenzBerechtigungenNaechsterVersuch', 0);
|
||||
$this->RegisterAttributeString('LizenzLetzterFehler', '');
|
||||
$this->RegisterAttributeString('PrognoseInstallationsToken', '');
|
||||
$this->RegisterAttributeString('StoerungsSnapshot', '{}');
|
||||
$this->RegisterAttributeString('StoerungsGesendeterHash', '');
|
||||
$this->RegisterAttributeInteger('StoerungsLetzterErfolg', 0);
|
||||
$this->RegisterAttributeInteger('StoerungsNaechsterVersuch', 0);
|
||||
$this->RegisterAttributeInteger('StoerungsFehlversuche', 0);
|
||||
|
||||
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
|
||||
$this->RegisterTimer('Regelzyklus', 0, '');
|
||||
@@ -152,6 +195,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'PrognoseSenden', true);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'StoerungenSenden',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'StoerungenSenden', true);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
@@ -169,6 +217,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', 0);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->SetTimerInterval('StoerungenSenden', 0);
|
||||
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
@@ -178,9 +227,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->SetTimerInterval(
|
||||
'StoerungenSenden',
|
||||
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 30000 : 0
|
||||
);
|
||||
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
||||
$this->aktualisiereAnschlussstatus();
|
||||
$geraetezugangFehlt = ($this->hatAnlagentopologie() || $this->prognoseTelemetrieKonfiguriert())
|
||||
$geraetezugangFehlt = ($this->hatAnlagentopologie()
|
||||
|| $this->prognoseTelemetrieKonfiguriert()
|
||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||
if (!$this->aktualisiereLizenzfreigabe(
|
||||
$this->ReadPropertyString('Lizenzcode'),
|
||||
@@ -207,6 +262,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
);
|
||||
$this->sendePrognoseTelemetrie();
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('StoerueberwachungAktiv')) {
|
||||
$this->sendeStoerungsSnapshot(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function GetConfigurationForm(): string
|
||||
@@ -347,6 +405,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->sendePrognoseTelemetrie();
|
||||
return;
|
||||
|
||||
case 'StoerungenSenden':
|
||||
$this->sendeStoerungsSnapshot(false);
|
||||
return;
|
||||
|
||||
case 'FormLizenzPruefen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
|
||||
@@ -1202,17 +1264,186 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
*/
|
||||
private function aktualisiereStoerungen(array $verbraucher, array $stoerungen): void
|
||||
{
|
||||
$meldungen = [];
|
||||
foreach (array_values(array_unique($stoerungen)) as $meldung) {
|
||||
$meldung = trim($meldung);
|
||||
if ($meldung === '') {
|
||||
continue;
|
||||
}
|
||||
$meldungen[] = [
|
||||
'sourceType' => 'manager',
|
||||
'sourceId' => 'ips:' . $this->InstanceID,
|
||||
'sourceName' => IPS_GetName($this->InstanceID),
|
||||
'code' => 'Manager.' . substr(hash('sha256', $meldung), 0, 16),
|
||||
'severity' => 'error',
|
||||
'message' => $meldung,
|
||||
];
|
||||
}
|
||||
|
||||
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']);
|
||||
if ($zustand['Art'] !== 'Stoerung' || $zustand['Wert'] !== true) {
|
||||
continue;
|
||||
}
|
||||
$instanzID = $eintrag['InstanzID'];
|
||||
$text = trim((string) ($zustand['Text'] ?? $zustand['Kennung']));
|
||||
$meldungen[] = [
|
||||
'sourceType' => 'consumer',
|
||||
'sourceId' => 'ips:' . $instanzID,
|
||||
'sourceName' => IPS_InstanceExists($instanzID)
|
||||
? IPS_GetName($instanzID)
|
||||
: 'Instanz ' . $instanzID,
|
||||
'code' => (string) $zustand['Kennung'],
|
||||
'severity' => 'error',
|
||||
'message' => $text !== '' ? $text : (string) $zustand['Kennung'],
|
||||
];
|
||||
}
|
||||
}
|
||||
$stoerungen = array_values(array_unique($stoerungen));
|
||||
$this->setzeDiagnosewert('Sammelstoerung', $stoerungen !== []);
|
||||
$this->setzeDiagnosewert('Stoertext', implode("\n", $stoerungen));
|
||||
|
||||
$snapshot = StoerungsSnapshot::erstellen(
|
||||
$this->initialisiereLizenzInstallationID(),
|
||||
$this->InstanceID,
|
||||
$this->stoerungsRolle(),
|
||||
(bool) $this->GetValue('Aktiv'),
|
||||
$meldungen
|
||||
);
|
||||
$this->WriteAttributeString(
|
||||
'StoerungsSnapshot',
|
||||
json_encode($snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
$texte = array_map(
|
||||
static fn (array $meldung): string => $meldung['sourceName'] . ': ' . $meldung['message'],
|
||||
$snapshot['faults']
|
||||
);
|
||||
$this->setzeDiagnosewert('Sammelstoerung', $texte !== []);
|
||||
$this->setzeDiagnosewert('Stoertext', implode("\n", $texte));
|
||||
$this->sendeStoerungsSnapshot(false);
|
||||
}
|
||||
|
||||
private function stoerungsRolle(): string
|
||||
{
|
||||
return [
|
||||
0 => 'standalone',
|
||||
1 => 'main',
|
||||
2 => 'submanager',
|
||||
][$this->ReadPropertyInteger('Rolle')] ?? 'standalone';
|
||||
}
|
||||
|
||||
private function sendeStoerungsSnapshot(bool $erzwingen): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('StoerueberwachungAktiv')) {
|
||||
$this->setzeDiagnosewert('Stoerueberwachungsstatus', 'NichtVerwendet');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$snapshot = $this->leseJsonAttribut('StoerungsSnapshot');
|
||||
if (($snapshot['version'] ?? null) !== StoerungsSnapshot::VERSION) {
|
||||
// Erst ein real erfasster Vollsnapshot darf bestehende Portalstoerungen aufloesen.
|
||||
$this->setzeDiagnosewert('Stoerueberwachungsstatus', 'Wartet');
|
||||
return;
|
||||
}
|
||||
$hash = StoerungsSnapshot::zustandHash($snapshot);
|
||||
$jetzt = time();
|
||||
if (!$erzwingen && $jetzt < $this->ReadAttributeInteger('StoerungsNaechsterVersuch')) {
|
||||
return;
|
||||
}
|
||||
if (!$erzwingen
|
||||
&& hash_equals($this->ReadAttributeString('StoerungsGesendeterHash'), $hash)
|
||||
&& ($jetzt - $this->ReadAttributeInteger('StoerungsLetzterErfolg'))
|
||||
< $this->ReadPropertyInteger('StoerungsSendeintervall')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('Der Geraetezugang fuer die Stoerueberwachung fehlt.');
|
||||
}
|
||||
$snapshot['capturedAt'] = gmdate('Y-m-d\TH:i:s\Z', $jetzt);
|
||||
$this->uebertrageStoerungsSnapshot(
|
||||
$this->ReadAttributeString('LizenzInstallationID'),
|
||||
$snapshot,
|
||||
$token
|
||||
);
|
||||
$this->WriteAttributeString('StoerungsGesendeterHash', $hash);
|
||||
$this->WriteAttributeInteger('StoerungsLetzterErfolg', $jetzt);
|
||||
$this->WriteAttributeInteger('StoerungsNaechsterVersuch', 0);
|
||||
$this->WriteAttributeInteger('StoerungsFehlversuche', 0);
|
||||
$this->setzeDiagnosewert('Stoerueberwachungsstatus', 'Verbunden');
|
||||
} catch (Throwable $fehler) {
|
||||
$versuche = $this->ReadAttributeInteger('StoerungsFehlversuche') + 1;
|
||||
$wartezeit = min(
|
||||
self::STOERUNGS_RETRY_MAX_SEKUNDEN,
|
||||
30 * (2 ** min($versuche - 1, 5))
|
||||
);
|
||||
$meldung = substr($fehler->getMessage(), 0, 500);
|
||||
$this->WriteAttributeInteger('StoerungsFehlversuche', $versuche);
|
||||
$this->WriteAttributeInteger('StoerungsNaechsterVersuch', time() + $wartezeit);
|
||||
$this->setzeDiagnosewert('Stoerueberwachungsstatus', 'Fehler: ' . $meldung);
|
||||
$this->protokolliere('Stoerueberwachung', $meldung);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $nutzlast */
|
||||
private function uebertrageStoerungsSnapshot(
|
||||
string $installationID,
|
||||
array $nutzlast,
|
||||
string $token
|
||||
): void {
|
||||
$url = sprintf(self::STOERUNGS_ENDPOINT, rawurlencode($installationID));
|
||||
$json = json_encode($nutzlast, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||
$curl = curl_init($url);
|
||||
if ($curl === false) {
|
||||
throw new RuntimeException('Die Stoerungsuebertragung konnte nicht vorbereitet werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_CUSTOMREQUEST => 'PUT',
|
||||
CURLOPT_POSTFIELDS => $json,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/json',
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_USERAGENT => 'Enelix-EMS-Manager/0.3',
|
||||
]);
|
||||
$antwort = curl_exec($curl);
|
||||
if ($antwort === false) {
|
||||
throw new RuntimeException(
|
||||
'Stoerungsdienst nicht erreichbar: ' . curl_error($curl)
|
||||
);
|
||||
}
|
||||
if (strlen($antwort) > 65536) {
|
||||
throw new RuntimeException('Die Antwort der Stoerungsuebertragung ist zu gross.');
|
||||
}
|
||||
$httpStatus = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
} finally {
|
||||
curl_close($curl);
|
||||
}
|
||||
|
||||
if ($httpStatus === 401 || $httpStatus === 403) {
|
||||
$this->WriteAttributeString('PrognoseInstallationsToken', '');
|
||||
}
|
||||
if ($httpStatus !== 200) {
|
||||
$meldung = 'Unbekannter Fehler';
|
||||
try {
|
||||
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (is_array($daten) && is_string($daten['error'] ?? null)) {
|
||||
$meldung = $daten['error'];
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
// Der HTTP-Status bleibt die fuehrende Fehlerinformation.
|
||||
}
|
||||
throw new RuntimeException(
|
||||
'Stoerungsuebertragung HTTP ' . $httpStatus . ': ' . $meldung
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function registriereNetzleistungsmeldung(): void
|
||||
@@ -1635,7 +1866,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$nutzlast = json_encode([
|
||||
'code' => $lizenzcode,
|
||||
'installationId' => $installationID,
|
||||
'issueDeviceToken' => $this->hatAnlagentopologie()
|
||||
'issueDeviceToken' => ($this->hatAnlagentopologie()
|
||||
|| $this->prognoseTelemetrieKonfiguriert()
|
||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '',
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||
$curl = curl_init(self::LIZENZ_ENDPOINT);
|
||||
@@ -1708,12 +1941,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
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');
|
||||
}
|
||||
$this->setzeDiagnosewert(
|
||||
'Stoerueberwachungsstatus',
|
||||
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
|
||||
);
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
@@ -1727,6 +1963,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
if ($intervall < 60 || $intervall > 3600) {
|
||||
throw new InvalidArgumentException('PrognoseSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
|
||||
}
|
||||
$stoerungsIntervall = $this->ReadPropertyInteger('StoerungsSendeintervall');
|
||||
if ($stoerungsIntervall < 60 || $stoerungsIntervall > 3600) {
|
||||
throw new InvalidArgumentException('StoerungsSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
|
||||
}
|
||||
$telemetrieIDs = [
|
||||
$this->ReadPropertyInteger('PrognosePVVariableID'),
|
||||
$this->ReadPropertyInteger('PrognoseHausverbrauchVariableID'),
|
||||
@@ -1771,7 +2011,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->leseAktiveVerbraucherIDs('AutomatischeVerbraucherZuordnung');
|
||||
$this->dekodiereMonatsgrenzen($this->ReadPropertyString('Monatsgrenzen'));
|
||||
$this->leseAnlagentopologie();
|
||||
foreach (['PrognoseAnschluss', 'SDLAnschluss', 'StoermeldeAnschluss'] as $property) {
|
||||
foreach (['PrognoseAnschluss', 'SDLAnschluss'] 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.');
|
||||
|
||||
Reference in New Issue
Block a user