This commit is contained in:
@@ -18,7 +18,9 @@ final class EinStufigRegler
|
||||
int $istleistung,
|
||||
bool $verfuegbar,
|
||||
bool $aenderungMoeglich,
|
||||
bool $mindestlaufzeitErzwungen
|
||||
bool $mindestlaufzeitErzwungen,
|
||||
bool $peakbetrieb,
|
||||
bool $peakSperreBeiMindestlaufzeitAnbieten = true
|
||||
): array {
|
||||
if ($nennleistung <= 0) {
|
||||
throw new InvalidArgumentException('Die Nennleistung muss groesser als 0 sein.');
|
||||
@@ -27,9 +29,17 @@ final class EinStufigRegler
|
||||
throw new InvalidArgumentException('Die Istleistung muss 0 oder der Nennleistung entsprechen.');
|
||||
}
|
||||
|
||||
if (!$verfuegbar || !$aenderungMoeglich || $mindestlaufzeitErzwungen) {
|
||||
if (!$verfuegbar || !$aenderungMoeglich) {
|
||||
return [$istleistung];
|
||||
}
|
||||
if ($mindestlaufzeitErzwungen) {
|
||||
return $peakbetrieb && $peakSperreBeiMindestlaufzeitAnbieten
|
||||
? [0, $nennleistung]
|
||||
: [$nennleistung];
|
||||
}
|
||||
if ($peakbetrieb) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
return [0, $nennleistung];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
final class LadestationAdapter
|
||||
{
|
||||
public const GO_E_ALT = 1;
|
||||
public const GO_E_GEMINI = 2;
|
||||
public const SMART_ME_PICO = 3;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $konfiguration
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function statusAnfrage(int $geraetetyp, array $konfiguration): array
|
||||
{
|
||||
if ($geraetetyp === self::GO_E_ALT) {
|
||||
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=');
|
||||
}
|
||||
if ($geraetetyp === self::GO_E_GEMINI) {
|
||||
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/api/status');
|
||||
}
|
||||
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||
$id = self::pflichtwert($konfiguration, 'GeraeteID');
|
||||
|
||||
return self::anfrage(
|
||||
'GET',
|
||||
'https://api.smart-me.com/pico/charging/' . rawurlencode($id),
|
||||
$konfiguration
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $konfiguration
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public static function steuerAnfragen(
|
||||
int $geraetetyp,
|
||||
array $konfiguration,
|
||||
int $ladestrom
|
||||
): array {
|
||||
if ($ladestrom < 0 || $ladestrom > 32 || ($ladestrom > 0 && $ladestrom < 6)) {
|
||||
throw new InvalidArgumentException('Ladestrom muss 0 oder 6 bis 32 A betragen.');
|
||||
}
|
||||
|
||||
if ($geraetetyp === self::GO_E_ALT) {
|
||||
$basis = self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=';
|
||||
if ($ladestrom === 0) {
|
||||
return [self::anfrage('GET', $basis . 'alw=0')];
|
||||
}
|
||||
|
||||
return [
|
||||
self::anfrage('GET', $basis . 'alw=1'),
|
||||
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
||||
];
|
||||
}
|
||||
|
||||
if ($geraetetyp === self::GO_E_GEMINI) {
|
||||
$basis = self::lokaleBasisUrl($konfiguration) . '/api/set?';
|
||||
if ($ladestrom === 0) {
|
||||
return [self::anfrage('GET', $basis . 'frc=1&sua=false')];
|
||||
}
|
||||
|
||||
return [
|
||||
self::anfrage('GET', $basis . 'frc=2'),
|
||||
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
||||
];
|
||||
}
|
||||
|
||||
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||
$seriennummer = self::pflichtwert($konfiguration, 'Seriennummer');
|
||||
|
||||
return [self::anfrage(
|
||||
'POST',
|
||||
'https://api.smart-me.com/pico/loadmanagementgroup/current/'
|
||||
. rawurlencode($seriennummer)
|
||||
. '?current=' . ($ladestrom * 1000),
|
||||
$konfiguration
|
||||
)];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{Rohstatus: int, Istleistung_W: float}
|
||||
*/
|
||||
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
||||
{
|
||||
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new RuntimeException('Die Geraeteantwort ist kein JSON-Objekt.');
|
||||
}
|
||||
|
||||
if ($geraetetyp === self::GO_E_ALT || $geraetetyp === self::GO_E_GEMINI) {
|
||||
if (!isset($daten['car'], $daten['nrg'])
|
||||
|| !is_numeric($daten['car'])
|
||||
|| !is_array($daten['nrg'])
|
||||
|| !isset($daten['nrg'][11])
|
||||
|| !is_numeric($daten['nrg'][11])
|
||||
) {
|
||||
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
|
||||
}
|
||||
|
||||
$faktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
|
||||
|
||||
return [
|
||||
'Rohstatus' => (int) $daten['car'],
|
||||
'Istleistung_W' => (float) $daten['nrg'][11] * $faktor,
|
||||
];
|
||||
}
|
||||
|
||||
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||
if (!isset($daten['State'], $daten['ActiveChargingPower'])
|
||||
|| !is_numeric($daten['State'])
|
||||
|| !is_numeric($daten['ActiveChargingPower'])
|
||||
) {
|
||||
throw new RuntimeException('Die Pico-Antwort enthaelt nicht alle Statusfelder.');
|
||||
}
|
||||
|
||||
return [
|
||||
'Rohstatus' => (int) $daten['State'],
|
||||
'Istleistung_W' => round((float) $daten['ActiveChargingPower'] * 1000.0),
|
||||
];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $konfiguration
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function anfrage(
|
||||
string $methode,
|
||||
string $url,
|
||||
array $konfiguration = []
|
||||
): array {
|
||||
$anfrage = ['Methode' => $methode, 'URL' => $url];
|
||||
if (str_starts_with($url, 'https://api.smart-me.com/')) {
|
||||
$anfrage['Benutzername'] = self::pflichtwert($konfiguration, 'Benutzername');
|
||||
$anfrage['Passwort'] = self::pflichtwert($konfiguration, 'Passwort');
|
||||
}
|
||||
|
||||
return $anfrage;
|
||||
}
|
||||
|
||||
/** @param array<string, string> $konfiguration */
|
||||
private static function lokaleBasisUrl(array $konfiguration): string
|
||||
{
|
||||
$adresse = trim(self::pflichtwert($konfiguration, 'Geraeteadresse'));
|
||||
if (str_contains($adresse, '/') || str_contains($adresse, '?')) {
|
||||
throw new InvalidArgumentException('Geraeteadresse muss ein Hostname oder eine IP-Adresse sein.');
|
||||
}
|
||||
|
||||
return 'http://' . $adresse;
|
||||
}
|
||||
|
||||
/** @param array<string, string> $konfiguration */
|
||||
private static function pflichtwert(array $konfiguration, string $name): string
|
||||
{
|
||||
$wert = trim($konfiguration[$name] ?? '');
|
||||
if ($wert === '') {
|
||||
throw new InvalidArgumentException($name . ' darf nicht leer sein.');
|
||||
}
|
||||
|
||||
return $wert;
|
||||
}
|
||||
}
|
||||
|
||||
final class LadestationTransport
|
||||
{
|
||||
/** @var callable(array<string, mixed>): string */
|
||||
private $anfrageAusfuehren;
|
||||
|
||||
/** @param callable(array<string, mixed>): string $anfrageAusfuehren */
|
||||
public function __construct(callable $anfrageAusfuehren)
|
||||
{
|
||||
$this->anfrageAusfuehren = $anfrageAusfuehren;
|
||||
}
|
||||
|
||||
/** @param array<string, string> $konfiguration */
|
||||
public function statusLesen(int $geraetetyp, array $konfiguration): string
|
||||
{
|
||||
return ($this->anfrageAusfuehren)(
|
||||
LadestationAdapter::statusAnfrage($geraetetyp, $konfiguration)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, string> $konfiguration */
|
||||
public function ladestromSetzen(
|
||||
int $geraetetyp,
|
||||
array $konfiguration,
|
||||
int $ladestrom
|
||||
): void {
|
||||
foreach (LadestationAdapter::steuerAnfragen($geraetetyp, $konfiguration, $ladestrom) as $anfrage) {
|
||||
($this->anfrageAusfuehren)($anfrage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class LadestationRegler
|
||||
{
|
||||
public const PHASEN_UNBEKANNT = 0;
|
||||
public const PHASEN_EIN = 1;
|
||||
public const PHASEN_DREI = 3;
|
||||
|
||||
/** @var list<int> */
|
||||
private const LEISTUNGEN_EINPHASIG = [
|
||||
1450, 1690, 1930, 2170, 2410, 2650, 2890, 3130, 3370,
|
||||
3610, 3850, 4090, 4330, 4570, 4810, 5050, 5290, 5530,
|
||||
5770, 6010, 6250, 6490, 6730, 6970, 7210, 7450, 7690,
|
||||
];
|
||||
|
||||
/** @var list<int> */
|
||||
private const LEISTUNGEN_DREIPHASIG = [
|
||||
4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700,
|
||||
10300, 11000, 11750, 12450, 13150, 13850, 14550, 15250,
|
||||
15900, 16600, 17300, 18000, 18700, 19400, 20100, 20800,
|
||||
21500, 22000,
|
||||
];
|
||||
|
||||
/**
|
||||
* Die Schwelle, die Voll-Erkennung und der Zuschlag von 2.5 A stammen
|
||||
* aus der Enelix-1-Ladestation und bleiben bewusst kompatibel.
|
||||
*
|
||||
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus
|
||||
* @return array<string, int|float|bool>
|
||||
*/
|
||||
public static function normalisiereStatus(array $rohstatus, bool $warVerbunden): array
|
||||
{
|
||||
$verbunden = $rohstatus['Rohstatus'] !== 1;
|
||||
$leistung = max(0.0, $rohstatus['Istleistung_W']);
|
||||
$phasen = self::PHASEN_UNBEKANNT;
|
||||
$ladestrom = 0.0;
|
||||
$geladen = false;
|
||||
|
||||
if ($verbunden) {
|
||||
$phasen = $leistung > 7500.0 ? self::PHASEN_DREI : self::PHASEN_EIN;
|
||||
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0;
|
||||
$ladestrom = $leistung / $nenner;
|
||||
$ermittelterMaximalstrom = 2.5 + $ladestrom;
|
||||
$geladen = $warVerbunden && $ermittelterMaximalstrom < 6.0;
|
||||
}
|
||||
|
||||
if (!$verbunden) {
|
||||
$fahrzeugstatus = 1;
|
||||
} elseif ($geladen) {
|
||||
$fahrzeugstatus = 4;
|
||||
} elseif ($leistung > 0.0) {
|
||||
$fahrzeugstatus = 3;
|
||||
} else {
|
||||
$fahrzeugstatus = 2;
|
||||
}
|
||||
|
||||
return [
|
||||
'FahrzeugVerbunden' => $verbunden,
|
||||
'FahrzeugGeladen' => $geladen,
|
||||
'Fahrzeugstatus' => $fahrzeugstatus,
|
||||
'Phasenzahl' => $phasen,
|
||||
'Istleistung_W' => $leistung,
|
||||
'Ladestrom_A' => $ladestrom,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
public static function leistungsangebot(
|
||||
bool $aktiv,
|
||||
bool $ladefreigabe,
|
||||
bool $verbunden,
|
||||
bool $geladen,
|
||||
int $phasenzahl,
|
||||
int $maximalerLadestrom,
|
||||
bool $solarladen,
|
||||
bool $peakbetrieb
|
||||
): array {
|
||||
if (!$aktiv || !$ladefreigabe || !$verbunden || $geladen) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
|
||||
if ($stufen === []) {
|
||||
return [0];
|
||||
}
|
||||
if ($solarladen) {
|
||||
return $peakbetrieb ? [0] : array_merge([0], $stufen);
|
||||
}
|
||||
if ($peakbetrieb) {
|
||||
return array_merge([0], $stufen);
|
||||
}
|
||||
|
||||
return [$stufen[count($stufen) - 1]];
|
||||
}
|
||||
|
||||
public static function stromFuerLeistung(int $leistung, int $phasenzahl): int
|
||||
{
|
||||
if ($leistung <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$stufen = self::stufenFuerPhasen($phasenzahl);
|
||||
if ($leistung < $stufen[0]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$strom = 6;
|
||||
foreach ($stufen as $index => $schwelle) {
|
||||
if ($leistung < $schwelle) {
|
||||
break;
|
||||
}
|
||||
$strom = 6 + $index;
|
||||
}
|
||||
|
||||
return min(32, $strom);
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private static function leistungsstufen(int $phasenzahl, int $maximalerLadestrom): array
|
||||
{
|
||||
if ($maximalerLadestrom < 6 || $maximalerLadestrom > 32) {
|
||||
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||
}
|
||||
|
||||
return array_slice(self::stufenFuerPhasen($phasenzahl), 0, $maximalerLadestrom - 5);
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private static function stufenFuerPhasen(int $phasenzahl): array
|
||||
{
|
||||
if ($phasenzahl === self::PHASEN_EIN) {
|
||||
return self::LEISTUNGEN_EINPHASIG;
|
||||
}
|
||||
if ($phasenzahl === self::PHASEN_DREI) {
|
||||
return self::LEISTUNGEN_DREIPHASIG;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ use InvalidArgumentException;
|
||||
final class ManagerRegler
|
||||
{
|
||||
public const BETRIEBSART_INAKTIV = 'Inaktiv';
|
||||
public const BETRIEBSART_PV = 'PV';
|
||||
public const BETRIEBSART_PEAK = 'Peak';
|
||||
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
|
||||
|
||||
@@ -8,7 +8,10 @@ use InvalidArgumentException;
|
||||
|
||||
final class Nachrichtenvertrag
|
||||
{
|
||||
public const VERSION = '3.0';
|
||||
public const VERSION = '4.0';
|
||||
|
||||
public const BETRIEBSART_PV = 'PV';
|
||||
public const BETRIEBSART_PEAK = 'Peak';
|
||||
|
||||
public const LEISTUNGSQUELLE_NICHT_VORHANDEN = 0;
|
||||
public const LEISTUNGSQUELLE_BERECHNET = 1;
|
||||
@@ -19,11 +22,12 @@ final class Nachrichtenvertrag
|
||||
*/
|
||||
public static function pruefeManagerdaten(array $daten): void
|
||||
{
|
||||
self::pruefeSchluessel($daten, ['Kopf', 'Sollleistung_W'], 'Managerdaten');
|
||||
self::pruefeSchluessel($daten, ['Kopf', 'Betriebsart', 'Sollleistung_W'], 'Managerdaten');
|
||||
self::pruefeKopf($daten['Kopf']);
|
||||
self::pruefeBetriebsart($daten['Betriebsart']);
|
||||
|
||||
if (!is_int($daten['Sollleistung_W'])) {
|
||||
throw new InvalidArgumentException('Sollleistung_W muss eine Ganzzahl sein.');
|
||||
if ($daten['Sollleistung_W'] !== null && !is_int($daten['Sollleistung_W'])) {
|
||||
throw new InvalidArgumentException('Sollleistung_W muss eine Ganzzahl oder null sein.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +40,7 @@ final class Nachrichtenvertrag
|
||||
$daten,
|
||||
[
|
||||
'Kopf',
|
||||
'Betriebsart',
|
||||
'PrioritaetPV',
|
||||
'PrioritaetPeak',
|
||||
'Leistungswerte_W',
|
||||
@@ -49,6 +54,7 @@ final class Nachrichtenvertrag
|
||||
);
|
||||
|
||||
self::pruefeKopf($daten['Kopf']);
|
||||
self::pruefeBetriebsart($daten['Betriebsart']);
|
||||
self::pruefePrioritaet($daten['PrioritaetPV'], 'PrioritaetPV');
|
||||
self::pruefePrioritaet($daten['PrioritaetPeak'], 'PrioritaetPeak');
|
||||
self::pruefeLeistungswerte($daten['Leistungswerte_W']);
|
||||
@@ -93,6 +99,18 @@ final class Nachrichtenvertrag
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed $betriebsart */
|
||||
private static function pruefeBetriebsart($betriebsart): void
|
||||
{
|
||||
if (!is_string($betriebsart) || !in_array(
|
||||
$betriebsart,
|
||||
[self::BETRIEBSART_PV, self::BETRIEBSART_PEAK],
|
||||
true
|
||||
)) {
|
||||
throw new InvalidArgumentException('Betriebsart muss PV oder Peak sein.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $prioritaet
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Belevo\EnelixEMS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class PufferspeicherRegler
|
||||
{
|
||||
public const MINIMUM_AUS = 0;
|
||||
public const MINIMUM_STATISCH = 1;
|
||||
public const MINIMUM_DIFFERENZ = 2;
|
||||
|
||||
/**
|
||||
* @return list<array{Stufe: int, Leistung_W: int, KontaktID: int}>
|
||||
*/
|
||||
public static function dekodiereLeistungsstufen(string $json): array
|
||||
{
|
||||
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($werte) || !self::istListe($werte)) {
|
||||
throw new InvalidArgumentException('LeistungsStufen muss eine JSON-Liste sein.');
|
||||
}
|
||||
|
||||
$ergebnis = [];
|
||||
$leistungen = [];
|
||||
$kontakte = [];
|
||||
foreach ($werte as $index => $eintrag) {
|
||||
if (!is_array($eintrag)) {
|
||||
throw new InvalidArgumentException('Jede Leistungsstufe muss ein Objekt sein.');
|
||||
}
|
||||
$stufe = $eintrag['Stufe'] ?? $index + 1;
|
||||
$leistung = $eintrag['Leistung'] ?? null;
|
||||
$kontakt = $eintrag['Schaltkontakt_Stufe'] ?? null;
|
||||
if (!is_int($stufe) || $stufe <= 0) {
|
||||
throw new InvalidArgumentException('Stufe muss eine positive Ganzzahl sein.');
|
||||
}
|
||||
if (!is_int($leistung) || $leistung <= 0) {
|
||||
throw new InvalidArgumentException('Leistung muss eine positive Ganzzahl sein.');
|
||||
}
|
||||
if (!is_int($kontakt) || $kontakt <= 0) {
|
||||
throw new InvalidArgumentException('Schaltkontakt_Stufe muss eine positive Variablen-ID sein.');
|
||||
}
|
||||
if (isset($leistungen[$leistung])) {
|
||||
throw new InvalidArgumentException('Leistungen muessen eindeutig sein.');
|
||||
}
|
||||
if (isset($kontakte[$kontakt])) {
|
||||
throw new InvalidArgumentException('Schaltkontakte duerfen nur einer Stufe zugeordnet sein.');
|
||||
}
|
||||
|
||||
$leistungen[$leistung] = true;
|
||||
$kontakte[$kontakt] = true;
|
||||
$ergebnis[] = [
|
||||
'Stufe' => $stufe,
|
||||
'Leistung_W' => $leistung,
|
||||
'KontaktID' => $kontakt,
|
||||
];
|
||||
}
|
||||
|
||||
usort(
|
||||
$ergebnis,
|
||||
static fn (array $links, array $rechts): int => $links['Leistung_W'] <=> $rechts['Leistung_W']
|
||||
);
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function leistungswerte(array $stufen): array
|
||||
{
|
||||
return array_merge([0], array_column($stufen, 'Leistung_W'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||
*/
|
||||
public static function stufeFuerLeistung(array $stufen, int $leistung): int
|
||||
{
|
||||
if ($leistung === 0) {
|
||||
return 0;
|
||||
}
|
||||
foreach ($stufen as $stufe) {
|
||||
if ($stufe['Leistung_W'] === $leistung) {
|
||||
return $stufe['Stufe'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Die Sollleistung entspricht keiner konfigurierten Leistungsstufe.');
|
||||
}
|
||||
|
||||
public static function solltemperatur(
|
||||
float $aussentemperatur,
|
||||
float $fusspunkt,
|
||||
float $steigung,
|
||||
float $minimum,
|
||||
float $maximum
|
||||
): float {
|
||||
if ($steigung < 0.0) {
|
||||
throw new InvalidArgumentException('HeizkurvenSteigung darf nicht negativ sein.');
|
||||
}
|
||||
if ($minimum > $maximum) {
|
||||
throw new InvalidArgumentException('Heizkurvenminimum darf nicht groesser als das Maximum sein.');
|
||||
}
|
||||
|
||||
$soll = $fusspunkt + $steigung * (20.0 - $aussentemperatur);
|
||||
|
||||
return min($maximum, max($minimum, $soll));
|
||||
}
|
||||
|
||||
public static function einschaltschwelle(
|
||||
float $solltemperatur,
|
||||
float $hysterese,
|
||||
int $minimumModus,
|
||||
float $statischeMindesttemperatur,
|
||||
float $mindesttemperaturDifferenz
|
||||
): float {
|
||||
if ($hysterese <= 0.0) {
|
||||
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
if ($minimumModus === self::MINIMUM_AUS) {
|
||||
return $solltemperatur - $hysterese;
|
||||
}
|
||||
if ($minimumModus === self::MINIMUM_STATISCH) {
|
||||
return min($solltemperatur, $statischeMindesttemperatur);
|
||||
}
|
||||
if ($minimumModus === self::MINIMUM_DIFFERENZ) {
|
||||
if ($mindesttemperaturDifferenz < 0.0) {
|
||||
throw new InvalidArgumentException('MindesttemperaturDifferenz darf nicht negativ sein.');
|
||||
}
|
||||
|
||||
return $solltemperatur - $mindesttemperaturDifferenz;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('MindesttemperaturModus muss 0, 1 oder 2 sein.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function leistungsangebot(
|
||||
bool $aktiv,
|
||||
bool $peakbetrieb,
|
||||
bool $temperaturenGueltig,
|
||||
float $puffertemperatur,
|
||||
float $einschaltschwelle,
|
||||
array $stufen
|
||||
): array {
|
||||
if (!$aktiv || $peakbetrieb || !$temperaturenGueltig || $stufen === []) {
|
||||
return [0];
|
||||
}
|
||||
if ($puffertemperatur < $einschaltschwelle) {
|
||||
return self::leistungswerte($stufen);
|
||||
}
|
||||
|
||||
return [0];
|
||||
}
|
||||
|
||||
public static function fusspunktAusWaermepumpenwert(
|
||||
float $waermepumpenSolltemperatur,
|
||||
float $aussentemperatur,
|
||||
float $steigung
|
||||
): float {
|
||||
if ($steigung < 0.0) {
|
||||
throw new InvalidArgumentException('HeizkurvenSteigung darf nicht negativ sein.');
|
||||
}
|
||||
|
||||
return $waermepumpenSolltemperatur - $steigung * (20.0 - $aussentemperatur);
|
||||
}
|
||||
|
||||
public static function pt1(float $bisher, float $eingang, int $deltaSekunden, int $zeitkonstante): float
|
||||
{
|
||||
if ($deltaSekunden <= 0 || $zeitkonstante <= 0) {
|
||||
throw new InvalidArgumentException('PT1-Zeiten muessen groesser als 0 sein.');
|
||||
}
|
||||
$alpha = $deltaSekunden / ($zeitkonstante + $deltaSekunden);
|
||||
|
||||
return $bisher + $alpha * ($eingang - $bisher);
|
||||
}
|
||||
|
||||
public static function restzeitBisLastwechsel(
|
||||
int $letzterLastwechsel,
|
||||
int $jetzt,
|
||||
int $sperrzeitSekunden
|
||||
): int {
|
||||
if ($sperrzeitSekunden < 0) {
|
||||
throw new InvalidArgumentException('LastwechselSperrzeit darf nicht negativ sein.');
|
||||
}
|
||||
if ($letzterLastwechsel <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return max(0, $sperrzeitSekunden - max(0, $jetzt - $letzterLastwechsel));
|
||||
}
|
||||
|
||||
public static function energieKWh(float $leistungW, int $dauerSekunden): float
|
||||
{
|
||||
if ($dauerSekunden < 0) {
|
||||
throw new InvalidArgumentException('Dauer darf nicht negativ sein.');
|
||||
}
|
||||
|
||||
return $leistungW * $dauerSekunden / 3600000.0;
|
||||
}
|
||||
|
||||
/** @param array<mixed> $wert */
|
||||
private static function istListe(array $wert): bool
|
||||
{
|
||||
return $wert === [] || array_keys($wert) === range(0, count($wert) - 1);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,40 @@ final class WarmwasserRegler
|
||||
return array_merge([0], array_column($stufen, 'Leistung_W'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $leistungswerte
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function leistungsangebot(
|
||||
bool $peakbetrieb,
|
||||
float $temperatur,
|
||||
float $mindesttemperatur,
|
||||
float $maximaltemperatur,
|
||||
float $hysterese,
|
||||
bool $stufeAktiv,
|
||||
array $leistungswerte
|
||||
): array {
|
||||
if ($hysterese < 0.0 || $leistungswerte === []) {
|
||||
throw new InvalidArgumentException('Hysterese und Leistungswerte sind ungueltig.');
|
||||
}
|
||||
|
||||
$mindestladung = $temperatur < $mindesttemperatur
|
||||
|| ($temperatur < $mindesttemperatur + $hysterese && $stufeAktiv);
|
||||
if ($peakbetrieb) {
|
||||
return $mindestladung ? $leistungswerte : [0];
|
||||
}
|
||||
if ($mindestladung) {
|
||||
return [max($leistungswerte)];
|
||||
}
|
||||
if ($temperatur < $maximaltemperatur - $hysterese
|
||||
|| ($temperatur < $maximaltemperatur && $stufeAktiv)
|
||||
) {
|
||||
return $leistungswerte;
|
||||
}
|
||||
|
||||
return [0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user