187 lines
10 KiB
PHP
187 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
require_once __DIR__ . '/NetzfahrplanV4Regeltest.php';
|
|
|
|
/** Dedicated short-lease battery input. Disabled until explicitly armed on BOTH modules. */
|
|
trait BatterieNetzfahrplanV4TestTrait
|
|
{
|
|
private function registriereV4BatterieTest(): void
|
|
{
|
|
$this->RegisterPropertyBoolean('NetzfahrplanV4RegeltestErlaubt', false);
|
|
$this->RegisterAttributeString('NetzfahrplanV4LetzteTestsitzung', '');
|
|
$this->RegisterPropertyString('NetzfahrplanV4GeraeteWatchdogNachweis', '');
|
|
$this->RegisterAttributeString('NetzfahrplanV4TestStatus', '{"status":"disabled"}');
|
|
$this->RegisterTimer('NetzfahrplanV4TestWatchdog', 0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'NetzfahrplanV4TestWatchdog', 0);");
|
|
}
|
|
|
|
private function v4BatterieTestSitzung(): array
|
|
{
|
|
$s = json_decode($this->GetBuffer('V4BatteryTrialSession') ?: '{}', true);
|
|
return is_array($s) ? $s : [];
|
|
}
|
|
|
|
private function v4BatterieTestAktiv(): bool
|
|
{
|
|
return ($this->v4BatterieTestSitzung()['active'] ?? false) === true;
|
|
}
|
|
|
|
/** Local test arming by the bound manager; never called by ordinary forecast traffic. */
|
|
public function V4BatterieTestStarten(string $json): void
|
|
{
|
|
$lock = 'ENELIX.V4.Battery.' . $this->InstanceID;
|
|
if (!IPS_SemaphoreEnter($lock, 1000)) throw new RuntimeException('Batterie beschaeftigt.');
|
|
try { $this->v4BatterieStartIntern($json); }
|
|
finally { IPS_SemaphoreLeave($lock); }
|
|
}
|
|
|
|
private function v4BatterieStartIntern(string $json): void
|
|
{
|
|
if (!$this->ReadPropertyBoolean('NetzfahrplanV4RegeltestErlaubt')) throw new RuntimeException('Batterietest lokal nicht erlaubt.');
|
|
$s = json_decode($json, true, 32, JSON_THROW_ON_ERROR);$now = time();
|
|
if (!is_array($s) || ($s['kind'] ?? null) !== 'local_control_trial_consent'
|
|
|| ($s['batteryInstanceId'] ?? null) !== $this->InstanceID || $this->v4BatterieTestAktiv()) {
|
|
throw new RuntimeException('Batterie-Testanmeldung ungueltig oder bereits aktiv.');
|
|
}
|
|
NetzfahrplanV4Regeltest::uuid($s['sessionId'] ?? null);
|
|
if ($this->ReadAttributeString('NetzfahrplanV4LetzteTestsitzung') === $s['sessionId']) throw new RuntimeException('Testsitzung bereits beendet/verwendet.');
|
|
NetzfahrplanV4Regeltest::uuid($s['installationId'] ?? null);
|
|
$manager = NetzfahrplanV4Regeltest::integer($s['managerId'] ?? null, 'Manager', 1, 99999);
|
|
if (!in_array($manager, $this->zugeordneteManagerIDs(), true)
|
|
|| IPS_GetProperty($manager, 'NetzfahrplanV4RegeltestErlaubt') !== true) {
|
|
throw new RuntimeException('Manager nicht fuer Batterietest freigegeben/zugeordnet.');
|
|
}
|
|
$evidence = $this->ReadPropertyString('NetzfahrplanV4GeraeteWatchdogNachweis');
|
|
if (strlen($evidence) < 8 || $evidence !== ($s['actuatorWatchdogEvidenceId'] ?? null)) {
|
|
throw new RuntimeException('Externer Geraete-Watchdog muss separat nachgewiesen sein.');
|
|
}
|
|
$end = NetzfahrplanV4Regeltest::integer($s['expiresAt'] ?? null, 'Testende', $now + 1, $now + 1800);
|
|
$s['maxChargeW'] = NetzfahrplanV4Regeltest::number($s['maxChargeW'] ?? null, 'Testladen', 1, 5000);
|
|
$s['maxDischargeW'] = NetzfahrplanV4Regeltest::number($s['maxDischargeW'] ?? null, 'Testentladen', 1, 5000);
|
|
if (!is_string($s['assetId'] ?? null) || $s['assetId'] === '') throw new RuntimeException('Batterie-ID fehlt.');
|
|
if (!$this->GetValue('Aktiv') || $this->ReadPropertyInteger('Batteriemanagement') !== 2) {
|
|
throw new RuntimeException('Batterie nicht unter ENELIX-Regelung.');
|
|
}
|
|
$this->WriteAttributeString('NetzfahrplanV4LetzteTestsitzung', $s['sessionId']);
|
|
$s['active'] = true;$s['lastSequence'] = 0;$s['lastWallClockAt'] = $now;
|
|
$s['monotonicDeadline'] = hrtime(true) / 1e9 + $end - $now;
|
|
$s['commandMonotonicDeadline'] = hrtime(true) / 1e9 + 10;
|
|
$this->SetBuffer('V4BatteryTrialCommand', '{}');
|
|
$this->SetBuffer('V4BatteryTrialSession', json_encode($s, JSON_THROW_ON_ERROR));
|
|
// Old ordinary values may not resume after the test without a NEW manager message.
|
|
$this->setzeZustand('SollwertGueltig', false);
|
|
$this->SetTimerInterval('NetzfahrplanV4TestWatchdog', 1000);
|
|
$this->WriteAttributeString('NetzfahrplanV4TestStatus', '{"status":"armed_waiting_command","physicalResponseVerified":false}');
|
|
}
|
|
|
|
public function V4BatterieTestBefehl(string $json): void
|
|
{
|
|
$lock = 'ENELIX.V4.Battery.' . $this->InstanceID;
|
|
if (!IPS_SemaphoreEnter($lock, 1000)) throw new RuntimeException('Batterie beschaeftigt.');
|
|
try { $this->v4BatterieBefehlIntern($json); }
|
|
finally { IPS_SemaphoreLeave($lock); }
|
|
}
|
|
|
|
private function v4BatterieBefehlIntern(string $json): void
|
|
{
|
|
$s = $this->v4BatterieTestSitzung();$now = time();$mono = hrtime(true) / 1e9;
|
|
try {
|
|
if (!$this->ReadPropertyBoolean('NetzfahrplanV4RegeltestErlaubt')) throw new RuntimeException('Batterietest gesperrt.');
|
|
$c = json_decode($json, true, 32, JSON_THROW_ON_ERROR);
|
|
NetzfahrplanV4Regeltest::pruefeBatteriebefehl($c, $s, $now, $mono);
|
|
$s['lastSequence'] = $c['sequence'];$s['lastWallClockAt'] = $now;
|
|
$s['commandMonotonicDeadline'] = $mono + min(10, $c['expiresAt'] - $now);
|
|
$this->SetBuffer('V4BatteryTrialCommand', json_encode($c, JSON_THROW_ON_ERROR));
|
|
$this->SetBuffer('V4BatteryTrialSession', json_encode($s, JSON_THROW_ON_ERROR));
|
|
$this->aktualisiereIntern(true);
|
|
if (!$this->v4BatterieTestAktiv() || $this->ReadAttributeString('Registerfehler') !== '') {
|
|
throw new RuntimeException('Batterie konnte Testbefehl nicht sicher uebernehmen.');
|
|
}
|
|
$this->WriteAttributeString('NetzfahrplanV4TestStatus', json_encode([
|
|
'status' => 'controller_command_accepted', 'sessionId' => $s['sessionId'],
|
|
'sourceShadowPlanId' => $c['sourceShadowPlanId'], 'sequence' => $c['sequence'],
|
|
'commandExpiresAt' => $c['expiresAt'], 'actualRequestedW' => $this->ReadAttributeInteger('LetzterSollwert'),
|
|
'physicalResponseVerified' => false], JSON_THROW_ON_ERROR));
|
|
} catch (Throwable $e) {
|
|
$this->beendeV4BatterieTest('Befehl abgelehnt', true);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/** Explicit local abort or ApplyChanges: revoke before any register action. */
|
|
private function beendeV4BatterieTest(string $reason, bool $writeNow): void
|
|
{
|
|
$oldStatus = json_decode($this->ReadAttributeString('NetzfahrplanV4TestStatus'), true);
|
|
$active = $this->v4BatterieTestAktiv() || in_array($oldStatus['status'] ?? '', ['controller_command_accepted', 'armed_waiting_command', 'stop_pending', 'stop_failed'], true);
|
|
$this->SetBuffer('V4BatteryTrialSession', '{}');$this->SetBuffer('V4BatteryTrialCommand', '{}');
|
|
$this->SetTimerInterval('NetzfahrplanV4TestWatchdog', 0);
|
|
if (!$active) return;
|
|
$this->setzeZustand('SollwertGueltig', false);$this->setzeZustand('Sollleistung', 0);
|
|
$report = ['status' => 'stop_pending', 'reason' => $reason, 'physicalResponseVerified' => false];
|
|
$this->WriteAttributeString('NetzfahrplanV4TestStatus', json_encode($report, JSON_THROW_ON_ERROR));
|
|
if ($writeNow) {
|
|
try { $this->schreibeRegister(0, true);$report['status'] = 'stopped'; }
|
|
catch (Throwable $e) {
|
|
$report['status'] = 'stop_failed';
|
|
$this->WriteAttributeString('NetzfahrplanV4TestStatus', json_encode($report, JSON_THROW_ON_ERROR));
|
|
throw $e;
|
|
}
|
|
$this->WriteAttributeString('NetzfahrplanV4TestStatus', json_encode($report, JSON_THROW_ON_ERROR));
|
|
}
|
|
}
|
|
|
|
public function V4BatterieTestStoppen(): void
|
|
{
|
|
$lock = 'ENELIX.V4.Battery.' . $this->InstanceID;
|
|
if (!IPS_SemaphoreEnter($lock, 1000)) throw new RuntimeException('Batterie beschaeftigt.');
|
|
try { $this->beendeV4BatterieTest('Lokaler Testabbruch', true); }
|
|
finally { IPS_SemaphoreLeave($lock); }
|
|
}
|
|
|
|
public function GetV4BatterieTestStatus(): string
|
|
{
|
|
return $this->ReadAttributeString('NetzfahrplanV4TestStatus');
|
|
}
|
|
|
|
/** Called inside the existing battery update, immediately before writing registers. */
|
|
private function v4BatterieTestZiel(array $measurements, bool $available, int $now): ?int
|
|
{
|
|
if (!$this->v4BatterieTestAktiv()) return null;
|
|
$s = $this->v4BatterieTestSitzung();$mono = hrtime(true) / 1e9;
|
|
try {
|
|
if (!$this->ReadPropertyBoolean('NetzfahrplanV4RegeltestErlaubt')) throw new RuntimeException('Lokale Testfreigabe entzogen.');
|
|
$c = json_decode($this->GetBuffer('V4BatteryTrialCommand') ?: '{}', true);
|
|
if ($now >= $s['expiresAt'] || $mono >= $s['monotonicDeadline'] || $now < $s['lastWallClockAt']) throw new RuntimeException('Testsitzung abgelaufen/Zeitsprung.');
|
|
if (!$c) {
|
|
if ($mono >= $s['commandMonotonicDeadline']) throw new RuntimeException('Kein Startbefehl innerhalb Watchdog.');
|
|
return 0;
|
|
}
|
|
$w = NetzfahrplanV4Regeltest::batterieZiel($c, $s, $measurements, [
|
|
'available' => $available, 'reserve' => $this->ReadPropertyFloat('ReserveLadezustand'),
|
|
'minimum' => $this->ReadPropertyFloat('MindestLadezustand'),
|
|
'blocked' => (bool) $this->leseZustand('HystereseAktiv'),
|
|
'rechargeLimitW' => $this->ReadPropertyBoolean('NachladenMitMaximalleistung') ? null : $this->ReadPropertyInteger('MaximaleNachladeleistung'),
|
|
], $now, $mono);
|
|
// Respect switching lock for increases/reversals; safety reductions and zero are immediate.
|
|
$last = $this->ReadAttributeInteger('LetzterSollwert');
|
|
if ($now < $this->ReadAttributeInteger('AenderungFreigabeAb') && $w !== $last) {
|
|
if ($last * $w < 0) $w = 0;
|
|
elseif (abs($w) > abs($last)) $w = $last;
|
|
}
|
|
$s['lastWallClockAt'] = $now;
|
|
$this->SetBuffer('V4BatteryTrialSession', json_encode($s, JSON_THROW_ON_ERROR));
|
|
return $w;
|
|
} catch (Throwable $e) {
|
|
$this->beendeV4BatterieTest($e->getMessage(), false);
|
|
return 0; // revoke stale value even if ordinary cached setpoint existed
|
|
}
|
|
}
|
|
}
|