* @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> */ 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 $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 $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> $wechselrichter * @return array */ 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> $inverters * @param list> $pvArrays * @param list> $batteries * @return list> */ 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> $inverters * @param list> $pvArrays * @param list> $batteries * @return array */ 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, ]; } }