RegisterPropertyBoolean('NetzfahrplanV4SchattenAktiv', false); $this->RegisterPropertyBoolean('NetzfahrplanV4NetzladenErlaubt', false); $this->RegisterPropertyString('NetzfahrplanV4BatterieOptionen', '{}'); $this->RegisterPropertyInteger('NetzfahrplanV4MessnachweisVariableID', 0); $this->RegisterPropertyString('NetzfahrplanV4BezugszaehlerQuellen', '[]'); $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'] as $key) { $manager[$key] = $this->ReadPropertyInteger($key); } foreach (['Netzleistungsfaktor', 'Lastspitzengrenze', 'Einspeisegrenze'] as $key) { $manager[$key] = $this->ReadPropertyFloat($key); } $manager['V4ControlPlanSource'] = $this->leseJsonAttribut('Netzfahrplan')['sourceModel'] ?? 'legacy-unspecified'; $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); $manager['V4BezugszaehlerQuellen'] = json_decode( $this->ReadPropertyString('NetzfahrplanV4BezugszaehlerQuellen'), 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', 'LadezustandHysterese', '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.'); } $object = IPS_GetObject($id); return ['value' => $value, 'updated' => (int) $after['VariableUpdated'], 'ident' => $object['ObjectIdent'], 'parentID' => (int) $object['ParentID']]; }; 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)); } } }