feat(manager): integrate forecast controls and merge V4 development
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.
This commit is contained in:
dh
2026-10-06 14:22:16 +00:00
207 changed files with 20340 additions and 11 deletions
+117
View File
@@ -12,6 +12,11 @@ require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
require_once __DIR__ . '/../libs/EnergieMessung.php';
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
require_once __DIR__ . '/../libs/ManagerSDLTrait.php';
require_once __DIR__ . '/../libs/NetzfahrplanV4Betriebsdaten.php';
require_once __DIR__ . '/../libs/ManagerNetzfahrplanV4Trait.php';
require_once __DIR__ . '/../libs/ManagerNetzfahrplanV4TestTrait.php';
require_once __DIR__ . '/../libs/ManagerNetzfahrplanV4AktivtestTrait.php';
require_once __DIR__ . '/../libs/ManagerPrognoseFormular.php';
use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler;
@@ -19,6 +24,7 @@ use Belevo\EnelixEMS\EnergieMessung;
use Belevo\EnelixEMS\ManagerEnergieTrait;
use Belevo\EnelixEMS\Lizenzpruefung;
use Belevo\EnelixEMS\ManagerRegler;
use Belevo\EnelixEMS\ManagerPrognoseFormular;
use Belevo\EnelixEMS\ManagerSchnittstelle;
use Belevo\EnelixEMS\Nachrichtenvertrag;
use Belevo\EnelixEMS\StoerungsSnapshot;
@@ -27,6 +33,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
{
use ManagerEnergieTrait;
use \Belevo\EnelixEMS\ManagerSDLTrait;
use \Belevo\EnelixEMS\ManagerNetzfahrplanV4Trait;
use \Belevo\EnelixEMS\ManagerNetzfahrplanV4TestTrait;
use \Belevo\EnelixEMS\ManagerNetzfahrplanV4AktivtestTrait;
private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201;
@@ -118,6 +127,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
public function Create(): void
{
parent::Create();
$this->registriereNetzfahrplanV4();
$this->registriereV4ManagerTest();
$this->registriereV4ManagerAktivtest();
$this->RegisterPropertyInteger('Rolle', 0);
$this->RegisterPropertyInteger('NetzleistungVariableID', 0);
@@ -257,8 +269,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
public function ApplyChanges(): void
{
parent::ApplyChanges();
$this->v4ManagerTestAbbrechen('Manager neu initialisiert');
$this->SetTimerInterval('NetzfahrplanV4Senden', 0);
$this->stoppeNetzfahrplanV4Vorschau();
$this->initialisiereLizenzInstallationID();
$this->konfiguriereNetzfahrplanV4Daten();
$this->aktualisiereVariablen();
try {
@@ -315,6 +331,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return;
}
$this->SetStatus(self::STATUS_AKTIV);
if ($this->ReadPropertyBoolean('NetzfahrplanV4SchattenAktiv')) {
$this->SetTimerInterval('NetzfahrplanV4Senden', 90000);
}
$this->konfiguriereNetzfahrplanV4Vorschau();
$this->synchronisiereAnlagentopologie();
if ($this->prognoseTelemetrieKonfiguriert()) {
$this->SetTimerInterval(
@@ -451,6 +471,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->ReadPropertyBoolean('SDLAktiv')
);
$formular = ManagerPrognoseFormular::integrieren($formular, $this->prognoseBedienzustand());
return json_encode($formular, JSON_THROW_ON_ERROR);
}
@@ -489,6 +510,48 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->regeln(false);
return;
case 'NetzfahrplanV4Empfangen':
$this->empfangeNetzfahrplanV4();
return;
case 'NetzfahrplanV4VorschauAktualisieren':
$this->aktualisiereNetzfahrplanV4Vorschau();
return;
case 'NetzfahrplanV4TestStart':
if (!is_string($wert)) throw new InvalidArgumentException('Explizite JSON-Testfreigabe erforderlich.');
$this->V4ManagerTestStarten($wert);
return;
case 'NetzfahrplanV4TestStop':
$this->V4ManagerTestStoppen();
return;
case 'NetzfahrplanV4Aktivtest':
$this->V4ManagerAktivtestSchalten((bool) $wert);
return;
case 'FormNetzfahrplanSchalten':
if (!is_bool($wert)) {
throw new InvalidArgumentException('Netzfahrplan-Schaltung muss ein boolescher Wert sein.');
}
if ($wert && !ManagerPrognoseFormular::startErlaubt($this->prognoseBedienzustand())) {
throw new RuntimeException('Netzfahrplan nicht freigegeben. Gespeicherte Konfiguration, Lizenz und Testfreigabe pruefen.');
}
try {
$this->V4ManagerAktivtestSchalten($wert);
} finally {
$this->aktualisierePrognoseBedienung();
}
return;
case 'NetzfahrplanV4DatenErfassen':
$this->ErfasseNetzfahrplanV4Daten();
return;
case 'NetzfahrplanV4Senden':
$this->sendeNetzfahrplanV4();
return;
case 'PrognoseSenden':
$this->sendePrognoseTelemetrie();
return;
@@ -520,6 +583,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (!$aktiv) {
$this->UpdateFormField('NetzfahrplanAktiv', 'value', false);
}
$this->aktualisierePrognoseBedienung($aktiv);
return;
case 'FormSDLAktiv':
@@ -727,6 +791,28 @@ class Manager extends IPSModule implements ManagerSchnittstelle
}
private function regeln(bool $erneuern): void
{
$lock = 'ENELIX.V4.Control.' . $this->InstanceID;
if (!IPS_SemaphoreEnter($lock, 0)) return;
try {
$trialBefore=(bool)$this->v4ManagerTestSession();
$this->regelnIntern($erneuern);
if ($trialBefore && !$this->v4ManagerTestSession() && $this->GetValue('Aktiv')
&& IPS_GetInstance($this->InstanceID)['InstanceStatus'] === self::STATUS_AKTIV) {
$this->WriteAttributeString('LetzteSollwerte','{}');
$this->regelnIntern(true); // exactly one fresh fallback, no cached pre-trial target
}
if ($this->v4ManagerTestSession()
&& (!$this->GetValue('Aktiv') || IPS_GetInstance($this->InstanceID)['InstanceStatus'] !== self::STATUS_AKTIV)) {
$this->v4ManagerTestAbbrechen('Manager nicht mehr betriebsbereit');
}
} catch (Throwable $e) {
$this->v4ManagerTestAbbrechen('Regelungsfehler');
throw $e;
} finally { IPS_SemaphoreLeave($lock); }
}
private function regelnIntern(bool $erneuern): void
{
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
$this->SetValue('Betriebsart', ManagerRegler::BETRIEBSART_INAKTIV);
@@ -831,6 +917,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
: $this->nichtGeregelteGeraeteText($nichtGeregelt)
);
$this->v4ManagerTestUebergabe($ergebnis, $verbraucher, $synchronisiert);
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
foreach ($ergebnis['Sollwerte'] as $instanzID => $sollleistung) {
if (!$erneuern && ($letzteSollwerte[(string) $instanzID] ?? null) === $sollleistung) {
@@ -840,6 +927,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->sendeManagerdaten((int) $instanzID, $betriebsart, $sollleistung);
$letzteSollwerte[(string) $instanzID] = $sollleistung;
} catch (Throwable $fehler) {
$this->v4ManagerTestAbbrechen('Andere Verbraucher-Stellwertuebergabe fehlgeschlagen');
$stoerungen[] = 'Sollwert an Instanz ' . $instanzID . ': ' . $fehler->getMessage();
}
}
@@ -2600,6 +2688,35 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$aktiv && $netzfahrplanLizenziert
&& $this->peakShavingLizenziert($lizenzcode)
);
$this->aktualisierePrognoseBedienung(null, $lizenzcode);
}
private function prognoseBedienzustand(?bool $aktiv = null, ?string $lizenzcode = null): array
{
return [
'prognoseAktiv' => ($aktiv ?? true) && $this->ReadPropertyBoolean('PrognoseAktiv'),
'lizenziert' => $this->berechtigungLizenziert(Lizenzpruefung::NETZFAHRPLAN, $lizenzcode),
'senderAktiv' => $this->ReadPropertyBoolean('NetzfahrplanV4SchattenAktiv'),
'empfangAktiv' => $this->ReadPropertyBoolean('NetzfahrplanV4EmpfangAktiv'),
'messdatenAktiv' => $this->ReadPropertyBoolean('NetzfahrplanV4MessdatenAktiv'),
'legacyAktiv' => $this->ReadPropertyBoolean('NetzfahrplanAktiv'),
'managerAktiv' => $this->GetValue('Aktiv')
&& IPS_GetInstance($this->InstanceID)['InstanceStatus'] === self::STATUS_AKTIV,
'testErlaubt' => $this->ReadPropertyBoolean('NetzfahrplanV4RegeltestErlaubt'),
'aktivtestErlaubt' => $this->ReadPropertyBoolean('NetzfahrplanV4AktivtestErlaubt'),
'watchdogVerzichtErlaubt' => $this->ReadPropertyBoolean('NetzfahrplanV4WatchdogVerzichtErlaubt'),
'aktivtestAktiv' => (bool) $this->GetValue('NetzfahrplanV4Aktivtest'),
'ende' => $this->ReadAttributeInteger('NetzfahrplanV4AktivtestEnde'),
'jetzt' => time(),
];
}
private function aktualisierePrognoseBedienung(?bool $aktiv = null, ?string $lizenzcode = null): void
{
$zustand = $this->prognoseBedienzustand($aktiv, $lizenzcode);
$this->UpdateFormField('PrognoseNetzfahrplanStart', 'enabled', ManagerPrognoseFormular::startErlaubt($zustand));
$this->UpdateFormField('PrognoseNetzfahrplanStatus', 'caption', ManagerPrognoseFormular::statustext($zustand));
$this->UpdateFormField('NetzfahrplanV4EmpfangAktiv', 'enabled', $zustand['lizenziert'] || $zustand['empfangAktiv']);
}
private function prognoseLizenzStatustext(bool $aktiv, bool $lizenziert): string