193 lines
6.5 KiB
PHP
193 lines
6.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use DateTimeImmutable;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Reine Hilfsfunktionen des mehrstufigen Warmwassererwaermers.
|
|
*
|
|
* Die Klasse kennt keine IP-Symcon-Funktionen und ist dadurch unabhaengig
|
|
* vom Laufzeitsystem testbar.
|
|
*/
|
|
final class WarmwasserRegler
|
|
{
|
|
/**
|
|
* @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 pt1(float $bisher, float $eingang, int $deltaSekunden, int $zeitkonstante): float
|
|
{
|
|
if ($deltaSekunden <= 0) {
|
|
throw new InvalidArgumentException('Das Zeitintervall muss groesser als 0 sein.');
|
|
}
|
|
if ($zeitkonstante <= 0) {
|
|
throw new InvalidArgumentException('Die Zeitkonstante muss groesser als 0 sein.');
|
|
}
|
|
|
|
$alpha = $deltaSekunden / ($zeitkonstante + $deltaSekunden);
|
|
|
|
return $bisher + $alpha * ($eingang - $bisher);
|
|
}
|
|
|
|
public static function energieKWh(float $leistungW, int $dauerSekunden): float
|
|
{
|
|
if ($dauerSekunden < 0) {
|
|
throw new InvalidArgumentException('Die Dauer darf nicht negativ sein.');
|
|
}
|
|
|
|
return $leistungW * $dauerSekunden / 3600000.0;
|
|
}
|
|
|
|
public static function kannZielErreichen(
|
|
float $volumenLiter,
|
|
float $isttemperatur,
|
|
float $zieltemperatur,
|
|
int $verbleibendeSekunden,
|
|
int $maximalleistungW
|
|
): bool {
|
|
if ($zieltemperatur <= $isttemperatur) {
|
|
return true;
|
|
}
|
|
if ($volumenLiter <= 0 || $verbleibendeSekunden < 0 || $maximalleistungW <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$erforderlicheWaermeJ = 4186.0 * $volumenLiter * ($zieltemperatur - $isttemperatur);
|
|
$verfuegbareWaermeJ = $maximalleistungW * $verbleibendeSekunden;
|
|
|
|
return $verfuegbareWaermeJ >= $erforderlicheWaermeJ;
|
|
}
|
|
|
|
/**
|
|
* @return array{Zeitpunkt: int, Solltemperatur: float}|null
|
|
*/
|
|
public static function naechsterZeitplaneintrag(string $json, DateTimeImmutable $jetzt): ?array
|
|
{
|
|
if (trim($json) === '') {
|
|
return null;
|
|
}
|
|
$werte = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!is_array($werte) || !self::istListe($werte)) {
|
|
throw new InvalidArgumentException('Zeitplan muss eine JSON-Liste sein.');
|
|
}
|
|
|
|
$naechster = null;
|
|
foreach ($werte as $eintrag) {
|
|
if (!is_array($eintrag)
|
|
|| !is_string($eintrag['Uhrzeit'] ?? null)
|
|
|| !is_numeric($eintrag['Solltemperatur'] ?? null)
|
|
|| !preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $eintrag['Uhrzeit'])
|
|
) {
|
|
throw new InvalidArgumentException('Zeitplaneintraege benoetigen Uhrzeit HH:MM und Solltemperatur.');
|
|
}
|
|
|
|
[$stunde, $minute] = array_map('intval', explode(':', $eintrag['Uhrzeit']));
|
|
$zeitpunkt = $jetzt->setTime($stunde, $minute, 0);
|
|
if ($zeitpunkt <= $jetzt) {
|
|
$zeitpunkt = $zeitpunkt->modify('+1 day');
|
|
}
|
|
if ($naechster === null || $zeitpunkt < $naechster['Datum']) {
|
|
$naechster = [
|
|
'Datum' => $zeitpunkt,
|
|
'Solltemperatur' => (float) $eintrag['Solltemperatur'],
|
|
];
|
|
}
|
|
}
|
|
|
|
if ($naechster === null) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'Zeitpunkt' => $naechster['Datum']->getTimestamp(),
|
|
'Solltemperatur' => $naechster['Solltemperatur'],
|
|
];
|
|
}
|
|
|
|
/** @param array<mixed> $wert */
|
|
private static function istListe(array $wert): bool
|
|
{
|
|
return $wert === [] || array_keys($wert) === range(0, count($wert) - 1);
|
|
}
|
|
}
|