Tests / test (push) Failing after 49s
Merge the V4 development history with current manager, SDL and setpoint fixes. Retain guarded trial behavior and consolidate controls in Prognose / Forecast. Exclude the unverified accounting-evidence change; no runtime deployment or new dispatch permission. Validated: 357 PHPUnit tests / 1758 assertions, 150 PHP syntax checks, 310 backend tests, 16 portal tests and isolated UI/receiver checks. develop and beta publication explicitly approved by Daniel Haefliger.
226 lines
9.9 KiB
PHP
226 lines
9.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
require_once __DIR__ . '/NetzfahrplanV4Regeltest.php';
|
|
|
|
/** Locally approved, time-boxed operation of validated V4 shadow plans on a test plant. */
|
|
trait ManagerNetzfahrplanV4AktivtestTrait
|
|
{
|
|
private const V4_AKTIVTEST_DAUER_SEKUNDEN = 172800;
|
|
private const V4_AKTIVTEST_WATCHDOG_VERZICHT = 'explicit_test_plant_watchdog_waiver';
|
|
|
|
private function registriereV4ManagerAktivtest(): void
|
|
{
|
|
$this->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);
|
|
}
|
|
}
|