feat: Anlagentopologie im Manager abbilden
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
|
||||
final class Anlagentopologie
|
||||
{
|
||||
public const VERSION = '1.0';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws JsonException
|
||||
*/
|
||||
public static function ausJson(
|
||||
string $wechselrichterJson,
|
||||
string $pvFlaechenJson,
|
||||
string $batterienJson,
|
||||
string $installationID
|
||||
): array {
|
||||
$wechselrichter = self::dekodiereListe($wechselrichterJson, 'AnlagenWechselrichter');
|
||||
$pvFlaechen = self::dekodiereListe($pvFlaechenJson, 'AnlagenPVFlaechen');
|
||||
$batterien = self::dekodiereListe($batterienJson, 'AnlagenBatterien');
|
||||
|
||||
$inverters = [];
|
||||
$inverterById = [];
|
||||
foreach ($wechselrichter as $index => $eintrag) {
|
||||
$pfad = 'Wechselrichter ' . ($index + 1);
|
||||
$id = self::kennung($eintrag['ID'] ?? null, $pfad . ': ID');
|
||||
if (isset($inverterById[$id])) {
|
||||
throw new InvalidArgumentException($pfad . ': ID ist mehrfach vorhanden.');
|
||||
}
|
||||
$typ = self::auswahl(
|
||||
$eintrag['Typ'] ?? null,
|
||||
['pv', 'battery', 'hybrid'],
|
||||
$pfad . ': Typ'
|
||||
);
|
||||
$acRatedKw = self::positiveZahl(
|
||||
$eintrag['ACNennleistung_kW'] ?? null,
|
||||
$pfad . ': AC-Nennleistung'
|
||||
);
|
||||
$inverter = [
|
||||
'id' => $id,
|
||||
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||
'type' => $typ,
|
||||
'acRatedKw' => $acRatedKw,
|
||||
'acExportLimitKw' => self::optionaleGrenze(
|
||||
$eintrag['ACEinspeisegrenze_kW'] ?? null,
|
||||
$acRatedKw,
|
||||
$pfad . ': AC-Einspeisegrenze'
|
||||
),
|
||||
'acImportLimitKw' => self::optionaleGrenze(
|
||||
$eintrag['ACBezugsgrenze_kW'] ?? null,
|
||||
$acRatedKw,
|
||||
$pfad . ': AC-Bezugsgrenze'
|
||||
),
|
||||
];
|
||||
$inverters[] = $inverter;
|
||||
$inverterById[$id] = $inverter;
|
||||
}
|
||||
|
||||
$pvArrays = [];
|
||||
$pvIds = [];
|
||||
foreach ($pvFlaechen as $index => $eintrag) {
|
||||
$pfad = 'PV-Flaeche ' . ($index + 1);
|
||||
$id = self::eindeutigeKennung($eintrag['ID'] ?? null, $pvIds, $pfad);
|
||||
$inverterId = self::kennung(
|
||||
$eintrag['WechselrichterID'] ?? null,
|
||||
$pfad . ': Wechselrichter-ID'
|
||||
);
|
||||
$inverter = self::wechselrichter($inverterById, $inverterId, $pfad);
|
||||
if (!in_array($inverter['type'], ['pv', 'hybrid'], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': benoetigt einen PV- oder Hybridwechselrichter.'
|
||||
);
|
||||
}
|
||||
$moduleCount = self::optionalePositiveGanzzahl(
|
||||
$eintrag['Modulanzahl'] ?? null,
|
||||
$pfad . ': Modulanzahl'
|
||||
);
|
||||
$modulePowerWp = self::optionalePositiveZahl(
|
||||
$eintrag['Modulleistung_Wp'] ?? null,
|
||||
$pfad . ': Modulleistung'
|
||||
);
|
||||
if (($moduleCount === null) !== ($modulePowerWp === null)) {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': Modulanzahl und Modulleistung muessen gemeinsam gepflegt werden.'
|
||||
);
|
||||
}
|
||||
$pvArrays[] = [
|
||||
'id' => $id,
|
||||
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||
'dcKwp' => self::positiveZahl(
|
||||
$eintrag['DCLeistung_kWp'] ?? null,
|
||||
$pfad . ': DC-Leistung'
|
||||
),
|
||||
'tiltDeg' => self::zahlImBereich(
|
||||
$eintrag['Neigung_Grad'] ?? null,
|
||||
0.0,
|
||||
90.0,
|
||||
$pfad . ': Neigung'
|
||||
),
|
||||
'azimuthDeg' => self::zahlImBereich(
|
||||
$eintrag['Azimut_Grad'] ?? null,
|
||||
-180.0,
|
||||
180.0,
|
||||
$pfad . ': Azimut'
|
||||
),
|
||||
'inverterId' => $inverterId,
|
||||
'mpptInput' => self::optionalerText($eintrag['MPPT'] ?? null),
|
||||
'moduleCount' => $moduleCount,
|
||||
'modulePowerWp' => $modulePowerWp,
|
||||
];
|
||||
}
|
||||
|
||||
$batteryItems = [];
|
||||
$batteryIds = [];
|
||||
foreach ($batterien as $index => $eintrag) {
|
||||
$pfad = 'Batterie ' . ($index + 1);
|
||||
$id = self::eindeutigeKennung($eintrag['ID'] ?? null, $batteryIds, $pfad);
|
||||
$inverterId = self::kennung(
|
||||
$eintrag['WechselrichterID'] ?? null,
|
||||
$pfad . ': Wechselrichter-ID'
|
||||
);
|
||||
$inverter = self::wechselrichter($inverterById, $inverterId, $pfad);
|
||||
$kopplung = self::auswahl(
|
||||
$eintrag['Kopplung'] ?? null,
|
||||
['ac', 'dc', 'hybrid'],
|
||||
$pfad . ': Kopplung'
|
||||
);
|
||||
if ($kopplung === 'ac' && !in_array($inverter['type'], ['battery', 'hybrid'], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': AC-Kopplung benoetigt einen Batterie- oder Hybridwechselrichter.'
|
||||
);
|
||||
}
|
||||
if ($kopplung !== 'ac' && $inverter['type'] !== 'hybrid') {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': DC- und Hybridkopplung benoetigen einen Hybridwechselrichter.'
|
||||
);
|
||||
}
|
||||
$nominalCapacityKwh = self::positiveZahl(
|
||||
$eintrag['Nennkapazitaet_kWh'] ?? null,
|
||||
$pfad . ': Nennkapazitaet'
|
||||
);
|
||||
$usableCapacityKwh = self::positiveZahl(
|
||||
$eintrag['Nutzkapazitaet_kWh'] ?? null,
|
||||
$pfad . ': Nutzkapazitaet'
|
||||
);
|
||||
if ($usableCapacityKwh > $nominalCapacityKwh) {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': Nutzkapazitaet darf die Nennkapazitaet nicht uebersteigen.'
|
||||
);
|
||||
}
|
||||
$batteryItems[] = [
|
||||
'id' => $id,
|
||||
'name' => self::name($eintrag['Name'] ?? null, $id),
|
||||
'nominalCapacityKwh' => $nominalCapacityKwh,
|
||||
'usableCapacityKwh' => $usableCapacityKwh,
|
||||
'maxChargeKw' => self::positiveZahl(
|
||||
$eintrag['MaxLadeleistung_kW'] ?? null,
|
||||
$pfad . ': maximale Ladeleistung'
|
||||
),
|
||||
'maxDischargeKw' => self::positiveZahl(
|
||||
$eintrag['MaxEntladeleistung_kW'] ?? null,
|
||||
$pfad . ': maximale Entladeleistung'
|
||||
),
|
||||
'inverterId' => $inverterId,
|
||||
'coupling' => $kopplung,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => self::VERSION,
|
||||
'installationId' => trim($installationID),
|
||||
'inverters' => $inverters,
|
||||
'pvArrays' => $pvArrays,
|
||||
'batteries' => $batteryItems,
|
||||
'sharedAcLimits' => self::gemeinsameAcGrenzen(
|
||||
$inverters,
|
||||
$pvArrays,
|
||||
$batteryItems
|
||||
),
|
||||
'summary' => self::zusammenfassung($inverters, $pvArrays, $batteryItems),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
private static function dekodiereListe(string $json, string $name): array
|
||||
{
|
||||
$daten = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)
|
||||
|| ($daten !== [] && array_keys($daten) !== range(0, count($daten) - 1))
|
||||
) {
|
||||
throw new InvalidArgumentException($name . ' muss eine JSON-Liste sein.');
|
||||
}
|
||||
foreach ($daten as $eintrag) {
|
||||
if (!is_array($eintrag)) {
|
||||
throw new InvalidArgumentException($name . ' enthaelt einen ungueltigen Eintrag.');
|
||||
}
|
||||
}
|
||||
|
||||
return $daten;
|
||||
}
|
||||
|
||||
/** @param array<string, bool> $verwendet */
|
||||
private static function eindeutigeKennung($wert, array &$verwendet, string $pfad): string
|
||||
{
|
||||
$id = self::kennung($wert, $pfad . ': ID');
|
||||
if (isset($verwendet[$id])) {
|
||||
throw new InvalidArgumentException($pfad . ': ID ist mehrfach vorhanden.');
|
||||
}
|
||||
$verwendet[$id] = true;
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
private static function kennung($wert, string $feld): string
|
||||
{
|
||||
if (!is_string($wert)
|
||||
|| preg_match('/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/', trim($wert)) !== 1
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
$feld . ' muss 1 bis 64 Zeichen aus Buchstaben, Zahlen, Punkt, Minus oder Unterstrich enthalten.'
|
||||
);
|
||||
}
|
||||
|
||||
return trim($wert);
|
||||
}
|
||||
|
||||
private static function name($wert, string $fallback): string
|
||||
{
|
||||
if ($wert === null || $wert === '') {
|
||||
return $fallback;
|
||||
}
|
||||
if (!is_string($wert) || strlen(trim($wert)) > 120) {
|
||||
throw new InvalidArgumentException('Name muss ein Text mit maximal 120 Zeichen sein.');
|
||||
}
|
||||
|
||||
return trim($wert);
|
||||
}
|
||||
|
||||
/** @param list<string> $werte */
|
||||
private static function auswahl($wert, array $werte, string $feld): string
|
||||
{
|
||||
if (!is_string($wert) || !in_array($wert, $werte, true)) {
|
||||
throw new InvalidArgumentException($feld . ' ist ungueltig.');
|
||||
}
|
||||
|
||||
return $wert;
|
||||
}
|
||||
|
||||
private static function positiveZahl($wert, string $feld): float
|
||||
{
|
||||
if (!is_int($wert) && !is_float($wert)) {
|
||||
throw new InvalidArgumentException($feld . ' muss eine Zahl sein.');
|
||||
}
|
||||
if (!is_finite((float) $wert) || (float) $wert <= 0.0) {
|
||||
throw new InvalidArgumentException($feld . ' muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
return (float) $wert;
|
||||
}
|
||||
|
||||
private static function optionalePositiveZahl($wert, string $feld): ?float
|
||||
{
|
||||
if ($wert === null || $wert === '' || $wert === 0 || $wert === 0.0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::positiveZahl($wert, $feld);
|
||||
}
|
||||
|
||||
private static function optionalePositiveGanzzahl($wert, string $feld): ?int
|
||||
{
|
||||
if ($wert === null || $wert === '' || $wert === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!is_int($wert) || $wert <= 0) {
|
||||
throw new InvalidArgumentException($feld . ' muss eine positive Ganzzahl sein.');
|
||||
}
|
||||
|
||||
return $wert;
|
||||
}
|
||||
|
||||
private static function zahlImBereich(
|
||||
$wert,
|
||||
float $minimum,
|
||||
float $maximum,
|
||||
string $feld
|
||||
): float {
|
||||
if ((!is_int($wert) && !is_float($wert))
|
||||
|| !is_finite((float) $wert)
|
||||
|| (float) $wert < $minimum
|
||||
|| (float) $wert > $maximum
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('%s muss zwischen %g und %g liegen.', $feld, $minimum, $maximum)
|
||||
);
|
||||
}
|
||||
|
||||
return (float) $wert;
|
||||
}
|
||||
|
||||
private static function optionaleGrenze($wert, float $nennleistung, string $feld): ?float
|
||||
{
|
||||
$grenze = self::optionalePositiveZahl($wert, $feld);
|
||||
if ($grenze !== null && $grenze > $nennleistung) {
|
||||
throw new InvalidArgumentException(
|
||||
$feld . ' darf die AC-Nennleistung nicht uebersteigen.'
|
||||
);
|
||||
}
|
||||
|
||||
return $grenze;
|
||||
}
|
||||
|
||||
private static function optionalerText($wert): ?string
|
||||
{
|
||||
if ($wert === null || $wert === '') {
|
||||
return null;
|
||||
}
|
||||
if (!is_string($wert) || strlen(trim($wert)) > 64) {
|
||||
throw new InvalidArgumentException('MPPT muss ein Text mit maximal 64 Zeichen sein.');
|
||||
}
|
||||
|
||||
return trim($wert);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, mixed>> $wechselrichter
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function wechselrichter(array $wechselrichter, string $id, string $pfad): array
|
||||
{
|
||||
if (!isset($wechselrichter[$id])) {
|
||||
throw new InvalidArgumentException(
|
||||
$pfad . ': referenzierter Wechselrichter ist nicht vorhanden.'
|
||||
);
|
||||
}
|
||||
|
||||
return $wechselrichter[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $inverters
|
||||
* @param list<array<string, mixed>> $pvArrays
|
||||
* @param list<array<string, mixed>> $batteries
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private static function gemeinsameAcGrenzen(
|
||||
array $inverters,
|
||||
array $pvArrays,
|
||||
array $batteries
|
||||
): array {
|
||||
$result = [];
|
||||
foreach ($inverters as $inverter) {
|
||||
if ($inverter['type'] !== 'hybrid') {
|
||||
continue;
|
||||
}
|
||||
$pvIds = [];
|
||||
foreach ($pvArrays as $pvArray) {
|
||||
if ($pvArray['inverterId'] === $inverter['id']) {
|
||||
$pvIds[] = $pvArray['id'];
|
||||
}
|
||||
}
|
||||
$batteryIds = [];
|
||||
foreach ($batteries as $battery) {
|
||||
if ($battery['inverterId'] === $inverter['id']) {
|
||||
$batteryIds[] = $battery['id'];
|
||||
}
|
||||
}
|
||||
if ($pvIds === [] && $batteryIds === []) {
|
||||
continue;
|
||||
}
|
||||
$result[] = [
|
||||
'inverterId' => $inverter['id'],
|
||||
'acRatedKw' => $inverter['acRatedKw'],
|
||||
'pvArrayIds' => $pvIds,
|
||||
'batteryIds' => $batteryIds,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $inverters
|
||||
* @param list<array<string, mixed>> $pvArrays
|
||||
* @param list<array<string, mixed>> $batteries
|
||||
* @return array<string, float>
|
||||
*/
|
||||
private static function zusammenfassung(
|
||||
array $inverters,
|
||||
array $pvArrays,
|
||||
array $batteries
|
||||
): array {
|
||||
$pvInverterIds = [];
|
||||
$pvDcKwp = 0.0;
|
||||
foreach ($pvArrays as $pvArray) {
|
||||
$pvDcKwp += $pvArray['dcKwp'];
|
||||
$pvInverterIds[$pvArray['inverterId']] = true;
|
||||
}
|
||||
$pvAcKw = 0.0;
|
||||
foreach ($inverters as $inverter) {
|
||||
if (isset($pvInverterIds[$inverter['id']])) {
|
||||
$pvAcKw += $inverter['acRatedKw'];
|
||||
}
|
||||
}
|
||||
$usableBatteryKwh = 0.0;
|
||||
$maxBatteryChargeKw = 0.0;
|
||||
$maxBatteryDischargeKw = 0.0;
|
||||
foreach ($batteries as $battery) {
|
||||
$usableBatteryKwh += $battery['usableCapacityKwh'];
|
||||
$maxBatteryChargeKw += $battery['maxChargeKw'];
|
||||
$maxBatteryDischargeKw += $battery['maxDischargeKw'];
|
||||
}
|
||||
|
||||
return [
|
||||
'pvDcKwp' => $pvDcKwp,
|
||||
'pvAcKw' => $pvAcKw,
|
||||
'usableBatteryKwh' => $usableBatteryKwh,
|
||||
'maxBatteryChargeKw' => $maxBatteryChargeKw,
|
||||
'maxBatteryDischargeKw' => $maxBatteryDischargeKw,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user