Files
Enelix-EMS/libs/ManagerRegler.php
T
dh 5f1bdbc0fa
Tests / test (push) Canceled after 0s
Enelix-2-Manager implementieren
Installierbares IP-Symcon-Manager-Modul mit testbarer Verteilungslogik, Verbraucher-Timeouts, Sollwertversand und aktualisierter Dokumentation.
2026-09-15 15:59:25 +00:00

253 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
/**
* Reine Regel- und Verteilfunktion des Managers.
*
* Die Klasse kennt keine IP-Symcon-Funktionen und kann deshalb unabhaengig
* vom Laufzeitsystem getestet werden. Alle Leistungen verwenden die
* Vorzeichenkonvention des Vertrags: positiv Bezug, negativ Abgabe.
*/
final class ManagerRegler
{
public const BETRIEBSART_INAKTIV = 'Inaktiv';
public const BETRIEBSART_PV = 'PV';
public const BETRIEBSART_PEAK = 'Peak';
/**
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
*/
public static function wirksameLastspitzengrenze(
int $modus,
float $konstanteGrenze,
array $monatsgrenzen,
int $monat
): ?float {
if ($modus === 0) {
return null;
}
if ($modus === 1) {
return $konstanteGrenze;
}
if ($modus !== 2) {
throw new InvalidArgumentException('Lastspitzenmodus muss 0, 1 oder 2 sein.');
}
if ($monat < 1 || $monat > 12 || !array_key_exists($monat, $monatsgrenzen)) {
throw new InvalidArgumentException('Fuer den aktuellen Monat fehlt eine Lastspitzengrenze.');
}
return (float) $monatsgrenzen[$monat];
}
public static function bestimmeBetriebsart(
bool $aktiv,
bool $netzleistungGueltig,
float $netzleistung,
string $bisherigeBetriebsart,
bool $peakAktiv,
float $umschaltschwelleUeberschuss,
float $umschaltschwelleLastspitze,
float $sollwertSolarladen,
float $wirksameLastspitzengrenze,
float $umschaltdifferenzProzent
): string {
if (!$aktiv || !$netzleistungGueltig) {
return self::BETRIEBSART_INAKTIV;
}
if (!$peakAktiv) {
return self::BETRIEBSART_PV;
}
if ($umschaltdifferenzProzent < 0 || $umschaltdifferenzProzent > 100) {
throw new InvalidArgumentException('Umschaltdifferenz muss zwischen 0 und 100 Prozent liegen.');
}
$kandidat = $bisherigeBetriebsart;
if ($netzleistung < $umschaltschwelleUeberschuss) {
$kandidat = self::BETRIEBSART_PV;
} elseif ($netzleistung > $umschaltschwelleLastspitze) {
$kandidat = self::BETRIEBSART_PEAK;
} elseif (!in_array($kandidat, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)) {
$kandidat = self::BETRIEBSART_PV;
}
if ($kandidat === $bisherigeBetriebsart
|| !in_array($bisherigeBetriebsart, [self::BETRIEBSART_PV, self::BETRIEBSART_PEAK], true)
|| $umschaltdifferenzProzent === 0.0
) {
return $kandidat;
}
$pvKorrektur = $sollwertSolarladen - $netzleistung;
$peakKorrektur = $wirksameLastspitzengrenze - $netzleistung;
$bezugswert = max(abs($pvKorrektur), abs($peakKorrektur), 1.0);
$differenzProzent = abs($pvKorrektur - $peakKorrektur) / $bezugswert * 100.0;
return $differenzProzent >= $umschaltdifferenzProzent
? $kandidat
: $bisherigeBetriebsart;
}
/**
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
* @return array{
* Verteilbudget_W: int,
* AktuelleVerbraucherleistung_W: int,
* NichtSteuerbar_W: int,
* Sollwerte: array<int, int>,
* Restabweichung_W: int
* }
*/
public static function verteile(
float $netzleistung,
float $zielnetzleistung,
array $verbraucher,
string $prioritaetsfeld
): array {
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
}
$aktuelleLeistung = 0;
$nichtSteuerbar = 0;
$steuerbar = [];
foreach ($verbraucher as $eintrag) {
self::pruefeVerteilEintrag($eintrag, $prioritaetsfeld);
$leistung = self::aktuelleLeistung($eintrag['Daten']);
$aktuelleLeistung += $leistung;
$daten = $eintrag['Daten'];
if (!$daten['Verfuegbar'] || !$daten['AenderungMoeglich'] || $daten['Leistungswerte_W'] === []) {
$nichtSteuerbar += $leistung;
continue;
}
$steuerbar[] = [
'InstanzID' => $eintrag['InstanzID'],
'Prioritaet' => $daten[$prioritaetsfeld],
'Leistungswerte_W' => $daten['Leistungswerte_W'],
];
}
usort($steuerbar, static function (array $links, array $rechts): int {
return [$links['Prioritaet'], $links['InstanzID']] <=> [$rechts['Prioritaet'], $rechts['InstanzID']];
});
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
$sollwerte = [];
$minimumSumme = 0;
foreach ($steuerbar as $eintrag) {
$minimum = self::kleinsterLeistungswert($eintrag['Leistungswerte_W']);
$sollwerte[$eintrag['InstanzID']] = $minimum;
$minimumSumme += $minimum;
}
$verfuegbar = max(0, $verteilbudget - $nichtSteuerbar - $minimumSumme);
foreach ($steuerbar as $eintrag) {
$instanzID = $eintrag['InstanzID'];
$minimum = $sollwerte[$instanzID];
$neuerSollwert = self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'],
$minimum + $verfuegbar
);
$verfuegbar -= $neuerSollwert - $minimum;
$sollwerte[$instanzID] = $neuerSollwert;
}
$zugeteilt = $nichtSteuerbar + array_sum($sollwerte);
return [
'Verteilbudget_W' => $verteilbudget,
'AktuelleVerbraucherleistung_W' => $aktuelleLeistung,
'NichtSteuerbar_W' => $nichtSteuerbar,
'Sollwerte' => $sollwerte,
'Restabweichung_W' => $verteilbudget - $zugeteilt,
];
}
/**
* @param array<string, mixed> $daten
*/
public static function angenommenerSollwert(array $daten): ?int
{
foreach ($daten['Zustand'] as $zustand) {
if ($zustand['Kennung'] === 'Sollleistung_W') {
return is_int($zustand['Wert']) ? $zustand['Wert'] : null;
}
}
return null;
}
/**
* @param array<string, mixed> $daten
*/
private static function aktuelleLeistung(array $daten): int
{
if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN
&& is_numeric($daten['Istleistung_W'])
) {
return (int) round((float) $daten['Istleistung_W']);
}
return self::angenommenerSollwert($daten) ?? 0;
}
/**
* @param array<string, mixed> $eintrag
*/
private static function pruefeVerteilEintrag(array $eintrag, string $prioritaetsfeld): void
{
if (!isset($eintrag['InstanzID'], $eintrag['Daten'])
|| !is_int($eintrag['InstanzID'])
|| $eintrag['InstanzID'] <= 0
|| !is_array($eintrag['Daten'])
) {
throw new InvalidArgumentException('Verbrauchereintrag ist ungueltig.');
}
Nachrichtenvertrag::pruefeVerbraucherdaten($eintrag['Daten']);
if (!array_key_exists($prioritaetsfeld, $eintrag['Daten'])) {
throw new InvalidArgumentException('Prioritaet fehlt im Verbrauchereintrag.');
}
}
/**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/
private static function kleinsterLeistungswert(array $leistungswerte): int
{
$erster = $leistungswerte[0];
return is_int($erster) ? $erster : $erster['Von_W'];
}
/**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/
private static function groessterLeistungswertBis(array $leistungswerte, int $obergrenze): int
{
$gewaehlt = self::kleinsterLeistungswert($leistungswerte);
foreach ($leistungswerte as $eintrag) {
if (is_int($eintrag)) {
if ($eintrag > $obergrenze) {
break;
}
$gewaehlt = $eintrag;
continue;
}
if ($eintrag['Von_W'] > $obergrenze) {
break;
}
$gewaehlt = min($eintrag['Bis_W'], $obergrenze);
}
return $gewaehlt;
}
}