334 lines
11 KiB
PHP
334 lines
11 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 = Nachrichtenvertrag::BETRIEBSART_PV;
|
|
public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_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 $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 < $sollwertSolarladen) {
|
|
$kandidat = self::BETRIEBSART_PV;
|
|
} elseif ($netzleistung > $wirksameLastspitzengrenze) {
|
|
$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
|
|
* @param array<int, float|int> $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh
|
|
* @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 $bezogeneEnergieWh = []
|
|
): 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['Leistungswerte_W'] === []) {
|
|
$nichtSteuerbar += $leistung;
|
|
continue;
|
|
}
|
|
|
|
$energie = $bezogeneEnergieWh[$eintrag['InstanzID']] ?? 0.0;
|
|
if ((!is_int($energie) && !is_float($energie))
|
|
|| (is_float($energie) && !is_finite($energie))
|
|
) {
|
|
throw new InvalidArgumentException('Bezogene Energie muss eine endliche Zahl sein.');
|
|
}
|
|
$steuerbar[] = [
|
|
'InstanzID' => $eintrag['InstanzID'],
|
|
'Prioritaet' => $daten[$prioritaetsfeld],
|
|
'Energiegruppe' => (int) round((float) $energie / 2000.0),
|
|
'Leistungswerte_W' => $daten['Leistungswerte_W'],
|
|
];
|
|
}
|
|
|
|
usort($steuerbar, static function (array $links, array $rechts): int {
|
|
return [
|
|
$links['Prioritaet'],
|
|
$links['Energiegruppe'],
|
|
$links['InstanzID'],
|
|
] <=> [
|
|
$rechts['Prioritaet'],
|
|
$rechts['Energiegruppe'],
|
|
$rechts['InstanzID'],
|
|
];
|
|
});
|
|
|
|
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
|
|
$sollwerte = [];
|
|
$ausgangssumme = 0;
|
|
foreach ($steuerbar as $eintrag) {
|
|
$ausgangswert = self::neutralerLeistungswert($eintrag['Leistungswerte_W']);
|
|
$sollwerte[$eintrag['InstanzID']] = $ausgangswert;
|
|
$ausgangssumme += $ausgangswert;
|
|
}
|
|
|
|
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
|
|
foreach ($steuerbar as $eintrag) {
|
|
$instanzID = $eintrag['InstanzID'];
|
|
$ausgangswert = $sollwerte[$instanzID];
|
|
if ($rest >= 0) {
|
|
$neuerSollwert = self::groessterLeistungswertBis(
|
|
$eintrag['Leistungswerte_W'],
|
|
$ausgangswert + $rest
|
|
);
|
|
} else {
|
|
$neuerSollwert = self::kleinsterLeistungswertAb(
|
|
$eintrag['Leistungswerte_W'],
|
|
$ausgangswert + $rest
|
|
);
|
|
}
|
|
$rest -= $neuerSollwert - $ausgangswert;
|
|
$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 neutralerLeistungswert(array $leistungswerte): int
|
|
{
|
|
$negativ = null;
|
|
$positiv = null;
|
|
foreach ($leistungswerte as $eintrag) {
|
|
if (is_int($eintrag)) {
|
|
if ($eintrag === 0) {
|
|
return 0;
|
|
}
|
|
$negativ = $eintrag < 0 ? max($negativ ?? $eintrag, $eintrag) : $negativ;
|
|
$positiv = $eintrag > 0 ? min($positiv ?? $eintrag, $eintrag) : $positiv;
|
|
continue;
|
|
}
|
|
if ($eintrag['Von_W'] <= 0 && $eintrag['Bis_W'] >= 0) {
|
|
return 0;
|
|
}
|
|
$negativ = $eintrag['Bis_W'] < 0
|
|
? max($negativ ?? $eintrag['Bis_W'], $eintrag['Bis_W'])
|
|
: $negativ;
|
|
$positiv = $eintrag['Von_W'] > 0
|
|
? min($positiv ?? $eintrag['Von_W'], $eintrag['Von_W'])
|
|
: $positiv;
|
|
}
|
|
|
|
if ($negativ === null) {
|
|
return $positiv;
|
|
}
|
|
if ($positiv === null) {
|
|
return $negativ;
|
|
}
|
|
|
|
return abs($negativ) <= abs($positiv) ? $negativ : $positiv;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
|
*/
|
|
private static function kleinsterLeistungswertAb(array $leistungswerte, int $untergrenze): int
|
|
{
|
|
foreach ($leistungswerte as $eintrag) {
|
|
if (is_int($eintrag)) {
|
|
if ($eintrag >= $untergrenze) {
|
|
return $eintrag;
|
|
}
|
|
continue;
|
|
}
|
|
if ($eintrag['Bis_W'] < $untergrenze) {
|
|
continue;
|
|
}
|
|
|
|
return max($eintrag['Von_W'], $untergrenze);
|
|
}
|
|
|
|
return self::neutralerLeistungswert($leistungswerte);
|
|
}
|
|
}
|