158 lines
8.6 KiB
PHP
158 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
/** Shadow telemetry only. Does not read/apply a V4 schedule or write actuators. */
|
|
trait ManagerNetzfahrplanV4Trait
|
|
{
|
|
private function registriereNetzfahrplanV4(): void
|
|
{
|
|
$this->RegisterPropertyBoolean('NetzfahrplanV4SchattenAktiv', false);
|
|
$this->RegisterPropertyBoolean('NetzfahrplanV4NetzladenErlaubt', false);
|
|
$this->RegisterPropertyString('NetzfahrplanV4BatterieOptionen', '{}');
|
|
$this->RegisterPropertyInteger('NetzfahrplanV4MessnachweisVariableID', 0);
|
|
$this->RegisterAttributeString('NetzfahrplanV4Sendestatus', '{"status":"disabled"}');
|
|
$this->RegisterTimer('NetzfahrplanV4Senden', 0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'NetzfahrplanV4Senden', true);");
|
|
}
|
|
|
|
public function GetNetzfahrplanV4Diagnose(): string
|
|
{
|
|
try {
|
|
$manager = [];
|
|
foreach (['NetzleistungVariableID', 'MesswertMaxAlter', 'VerbraucherTimeout',
|
|
'Lastspitzenmodus', 'NetzbezugEnergieVariableID'] as $key) {
|
|
$manager[$key] = $this->ReadPropertyInteger($key);
|
|
}
|
|
foreach (['Netzleistungsfaktor', 'Lastspitzengrenze', 'Einspeisegrenze'] as $key) {
|
|
$manager[$key] = $this->ReadPropertyFloat($key);
|
|
}
|
|
$manager['EinspeisebegrenzungAktiv'] = $this->ReadPropertyBoolean('EinspeisebegrenzungAktiv');
|
|
$manager['NetzladenErlaubt'] = $this->ReadPropertyBoolean('NetzfahrplanV4NetzladenErlaubt');
|
|
$manager['Monatsgrenzen'] = json_decode($this->ReadPropertyString('Monatsgrenzen'), true, 512, JSON_THROW_ON_ERROR);
|
|
$manager['BatterieOptionen'] = json_decode($this->ReadPropertyString('NetzfahrplanV4BatterieOptionen'), true, 512, JSON_THROW_ON_ERROR);
|
|
$assets = json_decode($this->ReadPropertyString('AnlagenBatterien'), true, 512, JSON_THROW_ON_ERROR);
|
|
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
|
$controllers = [];
|
|
foreach ($this->aktiveVerbraucherIDs() as $id) {
|
|
if (!IPS_InstanceExists($id)
|
|
|| IPS_GetInstance($id)['ModuleInfo']['ModuleID'] !== '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}') {
|
|
continue;
|
|
}
|
|
$c = ['InstanzID' => $id];
|
|
foreach (['LadezustandVariableID', 'MaxLadeleistungVariableID', 'MaxEntladeleistungVariableID',
|
|
'IstleistungVariableID', 'MindestLadezustand', 'ReserveLadezustand', 'MesswertMaxAlter', 'Batteriemanagement'] as $key) {
|
|
$c[$key] = IPS_GetProperty($id, $key);
|
|
}
|
|
$entry = $cache[(string) $id] ?? [];
|
|
$c['EmpfangenAm'] = $entry['EmpfangenAm'] ?? 0;
|
|
$c['Verfuegbar'] = $entry['Daten']['Verfuegbar'] ?? false;
|
|
foreach (($entry['Daten']['Zustand'] ?? []) as $state) {
|
|
if (($state['Kennung'] ?? '') === 'HystereseAktiv') {
|
|
$c['HystereseAktiv'] = $state['Wert'];
|
|
}
|
|
}
|
|
$controllers[] = $c;
|
|
}
|
|
$evidence = [];
|
|
$evidenceID = $this->ReadPropertyInteger('NetzfahrplanV4MessnachweisVariableID');
|
|
if ($evidenceID > 0) {
|
|
if (!IPS_VariableExists($evidenceID) || IPS_GetVariable($evidenceID)['VariableType'] !== 3) {
|
|
throw new RuntimeException('Der konfigurierte Messnachweis ist keine JSON-Stringvariable.');
|
|
}
|
|
$evidence = json_decode(GetValue($evidenceID), true, 512, JSON_THROW_ON_ERROR);
|
|
if (!is_array($evidence)) {
|
|
throw new RuntimeException('Ungueltiger Messnachweis.');
|
|
}
|
|
}
|
|
$read = static function (int $id): array {
|
|
if (!IPS_VariableExists($id)) {
|
|
throw new RuntimeException('Messvariable ' . $id . ' fehlt.');
|
|
}
|
|
$before = IPS_GetVariable($id);
|
|
$value = GetValue($id);
|
|
$after = IPS_GetVariable($id);
|
|
if ($before['VariableUpdated'] !== $after['VariableUpdated']) {
|
|
throw new RuntimeException('Messwert hat sich beim Lesen geaendert; naechsten Durchlauf abwarten.');
|
|
}
|
|
return ['value' => $value, 'updated' => (int) $after['VariableUpdated']];
|
|
};
|
|
return json_encode(NetzfahrplanV4Betriebsdaten::erstellen($manager, $assets, $controllers,
|
|
$read, $evidence, time()), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
|
} catch (Throwable $error) {
|
|
return json_encode(['status' => 'invalid_inputs', 'reason' => substr($error->getMessage(), 0, 300)], JSON_THROW_ON_ERROR);
|
|
}
|
|
}
|
|
|
|
public function GetNetzfahrplanV4Sendestatus(): string
|
|
{
|
|
return $this->ReadAttributeString('NetzfahrplanV4Sendestatus');
|
|
}
|
|
|
|
private function sendeNetzfahrplanV4(): void
|
|
{
|
|
if (!$this->ReadPropertyBoolean('NetzfahrplanV4SchattenAktiv')) {
|
|
$this->WriteAttributeString('NetzfahrplanV4Sendestatus', '{"status":"disabled"}');
|
|
return;
|
|
}
|
|
try {
|
|
if (!$this->berechtigungLizenziert(Lizenzpruefung::NETZFAHRPLAN)) {
|
|
throw new RuntimeException('Netzfahrplanberechtigung fehlt.');
|
|
}
|
|
$snapshot = $this->GetNetzfahrplanV4Diagnose();
|
|
$data = json_decode($snapshot, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!isset($data['operation'])) {
|
|
throw new RuntimeException($data['reason'] ?? 'Betriebsdaten unvollstaendig.');
|
|
}
|
|
// Decoding as objects preserves empty JSON dictionaries in the API contract.
|
|
$payload = json_decode($snapshot, false, 512, JSON_THROW_ON_ERROR);
|
|
if (!isset($payload->operation)) {
|
|
throw new RuntimeException('Keine konsistenten Betriebsdaten.');
|
|
}
|
|
$id = $this->ReadAttributeString('LizenzInstallationID');
|
|
$token = $this->ReadAttributeString('PrognoseInstallationsToken');
|
|
if (!preg_match('/^[0-9a-f-]{36}$/i', $id) || $token === '') {
|
|
throw new RuntimeException('Installations-ID oder bestehender Geraetezugang fehlt.');
|
|
}
|
|
$url = 'https://license.enelix.ch/api/v1/installations/' . rawurlencode($id) . '/prognosis/planner-v4/operation';
|
|
$handle = curl_init($url);
|
|
if ($handle === false) {
|
|
throw new RuntimeException('V4-Verbindung konnte nicht vorbereitet werden.');
|
|
}
|
|
try {
|
|
curl_setopt_array($handle, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 5,
|
|
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
|
|
CURLOPT_POSTFIELDS => json_encode($payload->operation, JSON_THROW_ON_ERROR),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json', 'Authorization: Bearer ' . $token]]);
|
|
$answer = curl_exec($handle);
|
|
$status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
|
if ($answer === false || $status !== 200) {
|
|
throw new RuntimeException('V4-Schattenanbindung HTTP ' . $status . '; bestehende Regelung unveraendert.');
|
|
}
|
|
if (strlen($answer) > 65536) {
|
|
throw new RuntimeException('V4-Antwort ist zu gross.');
|
|
}
|
|
$ack = json_decode($answer, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!in_array($ack['status'] ?? '', ['stored', 'duplicate', 'archived_older'], true)) {
|
|
throw new RuntimeException('Unerwartete V4-Annahmebestaetigung.');
|
|
}
|
|
} finally {
|
|
curl_close($handle);
|
|
}
|
|
$this->WriteAttributeString('NetzfahrplanV4Sendestatus', json_encode([
|
|
'status' => 'sent_shadow', 'capturedAt' => $payload->operation->observedAt,
|
|
'acceptance' => $ack['status'], 'warnings' => $data['warnings'], 'liveEnabled' => false], JSON_THROW_ON_ERROR));
|
|
} catch (Throwable $error) {
|
|
// Only this sender's diagnosis changes; never SetStatus, live plan or actuator commands.
|
|
$this->WriteAttributeString('NetzfahrplanV4Sendestatus', json_encode([
|
|
'status' => 'error', 'reason' => substr($error->getMessage(), 0, 300), 'liveEnabled' => false], JSON_THROW_ON_ERROR));
|
|
}
|
|
}
|
|
}
|