This commit is contained in:
+165
-1
@@ -22,6 +22,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
private const STATUS_LIZENZ_UNGUELTIG = 203;
|
||||
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
||||
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 LIZENZ_RETRY_SEKUNDEN = 3600;
|
||||
private const VM_UPDATE = 10603;
|
||||
private const VERBRAUCHER_LIZENZEN = [
|
||||
@@ -97,6 +98,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterPropertyString('AnlagenWechselrichter', '[]');
|
||||
$this->RegisterPropertyString('AnlagenPVFlaechen', '[]');
|
||||
$this->RegisterPropertyString('AnlagenBatterien', '[]');
|
||||
$this->RegisterPropertyInteger('PrognosePVVariableID', 0);
|
||||
$this->RegisterPropertyInteger('PrognoseHausverbrauchVariableID', 0);
|
||||
$this->RegisterPropertyInteger('PrognoseSOCVariableID', 0);
|
||||
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
|
||||
$this->RegisterPropertyString('PrognoseAnschluss', '{}');
|
||||
$this->RegisterPropertyString('SDLAnschluss', '{}');
|
||||
$this->RegisterPropertyString('Lizenzcode', '');
|
||||
@@ -130,6 +135,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'PrognoseSenden',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'PrognoseSenden', true);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
@@ -146,6 +156,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetTimerInterval('Regelzyklus', 0);
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', 0);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
@@ -154,9 +165,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetTimerInterval('Regelzyklus', 0);
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
||||
$this->aktualisiereAnschlussstatus();
|
||||
$geraetezugangFehlt = $this->hatAnlagentopologie()
|
||||
$geraetezugangFehlt = ($this->hatAnlagentopologie() || $this->prognoseTelemetrieKonfiguriert())
|
||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||
if (!$this->aktualisiereLizenzfreigabe(
|
||||
$this->ReadPropertyString('Lizenzcode'),
|
||||
@@ -176,6 +188,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
$this->synchronisiereAnlagentopologie();
|
||||
if ($this->prognoseTelemetrieKonfiguriert()) {
|
||||
$this->SetTimerInterval(
|
||||
'PrognoseSenden',
|
||||
$this->ReadPropertyInteger('PrognoseSendeintervall') * 1000
|
||||
);
|
||||
$this->sendePrognoseTelemetrie();
|
||||
}
|
||||
}
|
||||
|
||||
public function GetConfigurationForm(): string
|
||||
@@ -295,6 +314,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->regeln((bool) $wert);
|
||||
return;
|
||||
|
||||
case 'PrognoseSenden':
|
||||
$this->sendePrognoseTelemetrie();
|
||||
return;
|
||||
|
||||
case 'FormLizenzPruefen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
|
||||
@@ -1447,6 +1470,29 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||
}
|
||||
}
|
||||
$intervall = $this->ReadPropertyInteger('PrognoseSendeintervall');
|
||||
if ($intervall < 60 || $intervall > 3600) {
|
||||
throw new InvalidArgumentException('PrognoseSendeintervall muss zwischen 60 und 3600 Sekunden liegen.');
|
||||
}
|
||||
$telemetrieIDs = [
|
||||
$this->ReadPropertyInteger('PrognosePVVariableID'),
|
||||
$this->ReadPropertyInteger('PrognoseHausverbrauchVariableID'),
|
||||
$this->ReadPropertyInteger('PrognoseSOCVariableID'),
|
||||
];
|
||||
$konfigurierteIDs = array_values(array_filter($telemetrieIDs, static fn (int $id): bool => $id > 0));
|
||||
if ($konfigurierteIDs !== [] && count($konfigurierteIDs) !== count($telemetrieIDs)) {
|
||||
throw new InvalidArgumentException('Fuer die Prognose muessen PV, Hausverbrauch und SOC gemeinsam konfiguriert sein.');
|
||||
}
|
||||
foreach ($konfigurierteIDs as $variableID) {
|
||||
if (!IPS_VariableExists($variableID)) {
|
||||
throw new InvalidArgumentException('Eine konfigurierte Prognosevariable existiert nicht.');
|
||||
}
|
||||
}
|
||||
if ($konfigurierteIDs !== []
|
||||
&& !IPS_VariableExists($this->ReadPropertyInteger('NetzleistungVariableID'))
|
||||
) {
|
||||
throw new InvalidArgumentException('Die Netzleistungsvariable fuer die Prognose existiert nicht.');
|
||||
}
|
||||
if ($this->ReadPropertyFloat('Umschaltdifferenz') < 0
|
||||
|| $this->ReadPropertyFloat('Umschaltdifferenz') > 100
|
||||
) {
|
||||
@@ -1484,6 +1530,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|| $topologie['batteries'] !== [];
|
||||
}
|
||||
|
||||
private function prognoseTelemetrieKonfiguriert(): bool
|
||||
{
|
||||
return $this->ReadPropertyInteger('PrognosePVVariableID') > 0
|
||||
&& $this->ReadPropertyInteger('PrognoseHausverbrauchVariableID') > 0
|
||||
&& $this->ReadPropertyInteger('PrognoseSOCVariableID') > 0;
|
||||
}
|
||||
|
||||
private function synchronisiereAnlagentopologie(): void
|
||||
{
|
||||
try {
|
||||
@@ -1572,6 +1625,117 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
}
|
||||
}
|
||||
|
||||
private function sendePrognoseTelemetrie(): void
|
||||
{
|
||||
if (!$this->prognoseTelemetrieKonfiguriert()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
|
||||
if ($token === '') {
|
||||
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
|
||||
}
|
||||
|
||||
$werte = [
|
||||
'PV' => GetValue($this->ReadPropertyInteger('PrognosePVVariableID')),
|
||||
'Hausverbrauch' => GetValue($this->ReadPropertyInteger('PrognoseHausverbrauchVariableID')),
|
||||
'Netzleistung' => GetValue($this->ReadPropertyInteger('NetzleistungVariableID')),
|
||||
'SOC' => GetValue($this->ReadPropertyInteger('PrognoseSOCVariableID')),
|
||||
];
|
||||
foreach ($werte as $name => $wert) {
|
||||
if (!is_int($wert) && !is_float($wert)) {
|
||||
throw new RuntimeException('Der Prognosewert ' . $name . ' ist keine Zahl.');
|
||||
}
|
||||
$werte[$name] = (float) $wert;
|
||||
if (!is_finite($werte[$name])) {
|
||||
throw new RuntimeException('Der Prognosewert ' . $name . ' ist nicht endlich.');
|
||||
}
|
||||
}
|
||||
$werte['Netzleistung'] *= $this->ReadPropertyFloat('Netzleistungsfaktor');
|
||||
if ($werte['PV'] < 0.0 || $werte['Hausverbrauch'] < 0.0
|
||||
|| $werte['SOC'] < 0.0 || $werte['SOC'] > 100.0
|
||||
) {
|
||||
throw new RuntimeException('Mindestens ein Prognosewert liegt ausserhalb des gueltigen Bereichs.');
|
||||
}
|
||||
|
||||
$installationID = $this->ReadAttributeString('LizenzInstallationID');
|
||||
$nutzlast = [
|
||||
'version' => '1.0',
|
||||
'installationId' => $installationID,
|
||||
'capturedAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'values' => $werte,
|
||||
];
|
||||
$this->uebertragePrognoseTelemetrie($installationID, $nutzlast, $token);
|
||||
$this->setzeDiagnosewert('Prognosestatus', 'Verbunden');
|
||||
} catch (Throwable $fehler) {
|
||||
$meldung = substr($fehler->getMessage(), 0, 500);
|
||||
$this->setzeDiagnosewert('Prognosestatus', 'Fehler: ' . $meldung);
|
||||
$this->protokolliere('Prognosetelemetrie', $meldung);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $nutzlast */
|
||||
private function uebertragePrognoseTelemetrie(
|
||||
string $installationID,
|
||||
array $nutzlast,
|
||||
string $token
|
||||
): void {
|
||||
$url = sprintf(self::PROGNOSE_TELEMETRIE_ENDPOINT, rawurlencode($installationID));
|
||||
$json = json_encode(
|
||||
$nutzlast,
|
||||
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION
|
||||
);
|
||||
$curl = curl_init($url);
|
||||
if ($curl === false) {
|
||||
throw new RuntimeException('Die Prognosetelemetrie konnte nicht vorbereitet werden.');
|
||||
}
|
||||
|
||||
try {
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_POST => true,
|
||||
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.2',
|
||||
]);
|
||||
$antwort = curl_exec($curl);
|
||||
if ($antwort === false) {
|
||||
throw new RuntimeException('Prognosedienst nicht erreichbar: ' . curl_error($curl));
|
||||
}
|
||||
if (strlen($antwort) > 65536) {
|
||||
throw new RuntimeException('Die Antwort der Prognosetelemetrie 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(
|
||||
'Prognosetelemetrie HTTP ' . $httpStatus . ': ' . $meldung
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<int, float> */
|
||||
private function dekodiereMonatsgrenzen(string $json): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user