Installierbares IP-Symcon-Manager-Modul mit testbarer Verteilungslogik, Verbraucher-Timeouts, Sollwertversand und aktualisierter Dokumentation.
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user