RegisterPropertyBoolean('NetzfahrplanV4AktivtestErlaubt', false); $this->RegisterPropertyBoolean('NetzfahrplanV4WatchdogVerzichtErlaubt', false); $this->RegisterAttributeInteger('NetzfahrplanV4AktivtestBeginn', 0); $this->RegisterAttributeInteger('NetzfahrplanV4AktivtestEnde', 0); $this->RegisterVariableBoolean( 'NetzfahrplanV4Aktivtest', 'V4 Prognosemanager aktiv (Testbetrieb)', '~Switch', 952 ); $this->EnableAction('NetzfahrplanV4Aktivtest'); } public function V4ManagerAktivtestSchalten(bool $aktiv): void { if (!$aktiv) { $this->SetValue('NetzfahrplanV4Aktivtest', false); $this->V4ManagerTestStoppen(); return; } if ($this->GetValue('NetzfahrplanV4Aktivtest') && $this->v4ManagerTestSession()) { return; } $lock = 'ENELIX.V4.Control.' . $this->InstanceID; if (!IPS_SemaphoreEnter($lock, 1000)) { throw new RuntimeException('Regelung beschaeftigt.'); } $armedBattery = null; try { $this->v4ManagerTestBedingungen(); if (!$this->ReadPropertyBoolean('NetzfahrplanV4AktivtestErlaubt') || !$this->ReadPropertyBoolean('NetzfahrplanV4WatchdogVerzichtErlaubt')) { throw new RuntimeException('Aktivtest oder Watchdog-Verzicht ist lokal nicht freigegeben.'); } if ($this->v4ManagerTestSession()) { throw new RuntimeException('Eine V4-Regelsitzung ist bereits aktiv.'); } $envelope = json_decode( $this->ReadAttributeString('NetzfahrplanV4Empfang'), true, 64, JSON_THROW_ON_ERROR ); if (!is_array($envelope['response'] ?? null) || !is_int($envelope['receivedAt'] ?? null)) { throw new RuntimeException('Kein gueltiger aktueller V4-Plan empfangen.'); } [$operation, $actual] = $this->netzfahrplanV4LokaleVorschauwerte(); $assets = json_decode($this->ReadPropertyString('AnlagenBatterien'), true, 32, JSON_THROW_ON_ERROR); if (!is_array($assets) || count($assets) !== 1 || !is_string($assets[0]['ID'] ?? null)) { throw new RuntimeException('Aktivtest braucht genau eine eindeutig konfigurierte Batterie.'); } $asset = $assets[0]; $batteryID = $this->v4AktivtestBatterieinstanz($asset); $now = time(); $start = $this->ReadAttributeInteger('NetzfahrplanV4AktivtestBeginn'); $until = $this->ReadAttributeInteger('NetzfahrplanV4AktivtestEnde'); if ($start === 0 && $until === 0) { $start = $now; $until = $now + self::V4_AKTIVTEST_DAUER_SEKUNDEN; $this->WriteAttributeInteger('NetzfahrplanV4AktivtestBeginn', $start); $this->WriteAttributeInteger('NetzfahrplanV4AktivtestEnde', $until); } if ($start <= 0 || $until <= $now || $until - $start > self::V4_AKTIVTEST_DAUER_SEKUNDEN) { throw new RuntimeException('Die einmalige 48-Stunden-Aktivtestfreigabe ist abgelaufen.'); } [$maxChargeW, $maxDischargeW] = $this->v4AktivtestLeistungsgrenzen($operation, $asset['ID']); $session = $this->v4AktivtestUuid(); $s = [ 'kind' => 'local_active_test_consent', 'enabled' => true, 'legacyEnabled' => false, 'sessionId' => $session, 'installationId' => $this->ReadAttributeString('LizenzInstallationID'), 'managerId' => $this->InstanceID, 'batteryInstanceId' => $batteryID, 'assetId' => $asset['ID'], 'startedAt' => $start, 'expiresAt' => $until, 'maxChargeW' => $maxChargeW, 'maxDischargeW' => $maxDischargeW, 'watchdogWaived' => true, 'actuatorWatchdogEvidenceId' => self::V4_AKTIVTEST_WATCHDOG_VERZICHT, 'feedbackConfigHash' => $operation['localFeedback'][$asset['ID']]['configHash'] ?? '', 'gridLimits' => NetzfahrplanV4Planpruefung::kontext($operation)['limits'], 'sequence' => 1, 'lastWallClockAt' => $now, 'monotonicDeadline' => hrtime(true) / 1e9 + $until - $now, ]; $response = $this->v4AktivtestAntwort($envelope['response'], $s, $now); NetzfahrplanV4Regeltest::befehl( $response, $operation, $actual, $s, $now, $envelope['receivedAt'] ); $this->WriteAttributeString('NetzfahrplanV4LetzteTestsitzung', $session); $armedBattery = $batteryID; $this->v4ManagerBatterieAktion($batteryID, 'NetzfahrplanV4TestStart', json_encode($s, JSON_THROW_ON_ERROR)); $this->SetBuffer('V4ManagerTrialSession', json_encode($s, JSON_THROW_ON_ERROR)); $this->SetTimerInterval('NetzfahrplanV4TestTick', 2000); $this->SetValue('NetzfahrplanV4Aktivtest', true); $this->WriteAttributeString('NetzfahrplanV4RegeltestStatus', json_encode([ 'status' => 'active_test_started', 'sessionId' => $session, 'expiresAt' => $until, 'watchdogWaivedForTestPlant' => true, 'physicalResponseVerified' => false, ], JSON_THROW_ON_ERROR)); } catch (Throwable $error) { $this->SetValue('NetzfahrplanV4Aktivtest', false); $this->SetBuffer('V4ManagerTrialSession', '{}'); $this->SetTimerInterval('NetzfahrplanV4TestTick', 0); if ($armedBattery !== null) { $this->v4ManagerBatterieAktion($armedBattery, 'NetzfahrplanV4TestStop', 0); } throw $error; } finally { IPS_SemaphoreLeave($lock); } } private function v4AktivtestLeistungsgrenzen(array $operation, string $assetId): array { foreach ($operation['batteries'] ?? [] as $battery) { if (($battery['id'] ?? null) !== $assetId) { continue; } return [ NetzfahrplanV4Regeltest::number($battery['maxChargeW'] ?? null, 'Anlagen-Ladegrenze', 1, 1e9), NetzfahrplanV4Regeltest::number($battery['maxDischargeW'] ?? null, 'Anlagen-Entladegrenze', 1, 1e9), ]; } throw new RuntimeException('Dynamische Leistungsgrenzen der Testbatterie fehlen.'); } private function v4AktivtestBatterieinstanz(array $asset): int { $matches = []; foreach ($this->aktiveVerbraucherIDs() as $id) { if (!IPS_InstanceExists($id) || IPS_GetInstance($id)['ModuleInfo']['ModuleID'] !== '{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}') { continue; } if (($asset['SOCVariableID'] ?? null) === IPS_GetProperty($id, 'LadezustandVariableID') && ($asset['LeistungVariableID'] ?? null) === IPS_GetProperty($id, 'IstleistungVariableID')) { $matches[] = $id; } } if (count($matches) !== 1) { throw new RuntimeException('Physische Batteriezuordnung ist nicht eindeutig.'); } return $matches[0]; } private function v4AktivtestAntwort(array $response, array $session, int $now): array { $plan = $response['plan'] ?? null; if (!is_array($plan)) { throw new RuntimeException('V4-Plan fehlt.'); } $validUntil = min($now + 90, (int) $session['expiresAt']); $quality = $plan['inputQuality'] ?? []; $response['controlledTrial'] = [ 'version' => 1, 'kind' => 'controlled_trial_authority', 'sourcePlanRemainsShadow' => true, 'installationId' => $session['installationId'], 'sessionId' => $session['sessionId'], 'managerId' => $session['managerId'], 'batteryInstanceId' => $session['batteryInstanceId'], 'assetId' => $session['assetId'], 'sourceShadowPlanId' => $plan['planId'] ?? null, 'revision' => $plan['configRevision'] ?? null, 'family' => $plan['sourceFamily'] ?? null, 'issuedAt' => gmdate('c', (int) $session['startedAt']), 'expiresAt' => gmdate('c', (int) $session['expiresAt']), 'checkedAt' => gmdate('c', $now), 'validUntil' => gmdate('c', $validUntil), 'maxChargeW' => $session['maxChargeW'], 'maxDischargeW' => $session['maxDischargeW'], 'accountingEvidenceId' => $quality['accountingEvidenceId'] ?? null, 'actuatorWatchdogEvidenceId' => self::V4_AKTIVTEST_WATCHDOG_VERZICHT, 'acceptEstimatedPeak' => true, 'controlContext' => $plan['controlContext'] ?? null, ]; return $response; } private function v4AktivtestUuid(): 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 substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20); } }