This commit is contained in:
@@ -0,0 +1,898 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../libs/VerbraucherSchnittstelle.php';
|
||||
require_once __DIR__ . '/../libs/VerbraucherBasisTrait.php';
|
||||
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
|
||||
require_once __DIR__ . '/../libs/WarmwasserRegler.php';
|
||||
|
||||
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
||||
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
||||
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
||||
use Belevo\EnelixEMS\WarmwasserRegler;
|
||||
|
||||
class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSchnittstelle
|
||||
{
|
||||
use VerbraucherBasisTrait;
|
||||
|
||||
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
||||
private const STATUS_AKTIV = 102;
|
||||
private const STATUS_TEMPERATUR_UNGUELTIG = 201;
|
||||
private const STATUS_KONFIGURATION_UNGUELTIG = 202;
|
||||
private const STATUS_SCHALTFEHLER = 203;
|
||||
private const VM_UPDATE = 10603;
|
||||
|
||||
/** @var list<string> */
|
||||
private const DIAGNOSE_VARIABLEN = [
|
||||
'Leistungsquelle',
|
||||
'SollwertGueltig',
|
||||
'Verfuegbar',
|
||||
'AenderungMoeglich',
|
||||
'TemperaturGueltig',
|
||||
'NachladenAktiv',
|
||||
'LegionellenbetriebAktiv',
|
||||
'LegioCounter',
|
||||
'Idle',
|
||||
'IdleCounter',
|
||||
];
|
||||
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->registriereVerbraucherBasis();
|
||||
|
||||
// Aus Boiler_x_Stufig uebernommene und sicher ergaenzte Konfiguration.
|
||||
$this->RegisterPropertyString('LeistungsStufen', '[]');
|
||||
$this->RegisterPropertyInteger('ZeitKonstante', 120);
|
||||
$this->RegisterPropertyInteger('Boilerfuehler_PT1', 0);
|
||||
$this->RegisterPropertyBoolean('BoilertemperaturGlaetten', false);
|
||||
$this->RegisterPropertyInteger('Boilervolumen', 300);
|
||||
$this->RegisterPropertyString('Zeitplan', '[]');
|
||||
$this->RegisterPropertyInteger('Interval', 5);
|
||||
$this->RegisterPropertyInteger('IdleCounterMax', 2);
|
||||
$this->RegisterPropertyInteger('TemperaturMaxAlter', 30);
|
||||
$this->RegisterPropertyFloat('Hysterese', 5.0);
|
||||
$this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true);
|
||||
$this->RegisterPropertyInteger('LegionellenIntervallTage', 7);
|
||||
$this->RegisterPropertyInteger('LegionellenHaltezeit', 0);
|
||||
$this->RegisterPropertyInteger('LegionellenMaximaldauer', 86400);
|
||||
$this->RegisterPropertyInteger('NachtstartStunde', 22);
|
||||
$this->RegisterPropertyInteger('NachtendeStunde', 7);
|
||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||
|
||||
$this->RegisterVariableFloat('Boilertemperatur', 'Boilertemperatur', '~Temperature', 100);
|
||||
$this->RegisterVariableInteger('AktiveStufe', 'Aktive Stufe', '', 110);
|
||||
$this->RegisterVariableFloat('BezogeneEnergie', 'Bezogene Energie', '~Electricity', 120);
|
||||
|
||||
$this->RegisterVariableFloat('Mindesttemperatur', 'Mindesttemperatur', '~Temperature', 130);
|
||||
$this->RegisterVariableFloat('Maximaltemperatur', 'Maximaltemperatur', '~Temperature', 140);
|
||||
$this->RegisterVariableFloat('Legionellentemperatur', 'Legionellentemperatur', '~Temperature', 150);
|
||||
$this->EnableAction('Mindesttemperatur');
|
||||
$this->EnableAction('Maximaltemperatur');
|
||||
$this->EnableAction('Legionellentemperatur');
|
||||
|
||||
$this->RegisterVariableBoolean('TemperaturGueltig', 'Temperatur gueltig', '~Switch', 200);
|
||||
$this->RegisterVariableBoolean('NachladenAktiv', 'Nachladen aktiv', '~Switch', 210);
|
||||
$this->RegisterVariableBoolean(
|
||||
'LegionellenbetriebAktiv',
|
||||
'Legionellenbetrieb aktiv',
|
||||
'~Switch',
|
||||
220
|
||||
);
|
||||
$this->RegisterVariableInteger('LegioCounter', 'Zeit seit Legionellenabschluss', '', 230);
|
||||
$this->RegisterVariableBoolean('Idle', 'Idle', '~Switch', 240);
|
||||
$this->RegisterVariableInteger('IdleCounter', 'Idle Counter', '', 250);
|
||||
|
||||
$this->RegisterAttributeBoolean('InitialwerteGesetzt', false);
|
||||
$this->RegisterAttributeInteger('RegistrierteFuehlerID', 0);
|
||||
$this->RegisterAttributeInteger('LetzteBerechnung', 0);
|
||||
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||
$this->RegisterAttributeInteger('LetzterLegionellenabschluss', 0);
|
||||
$this->RegisterAttributeInteger('LegionellenStart', 0);
|
||||
$this->RegisterAttributeInteger('LegionellenSeitZiel', 0);
|
||||
$this->RegisterAttributeFloat('Glaettungswert', 0.0);
|
||||
$this->RegisterAttributeBoolean('GlaettungInitialisiert', false);
|
||||
$this->RegisterAttributeString('Leistungsangebot', '[]');
|
||||
$this->RegisterAttributeString('Schaltfehler', '');
|
||||
$this->RegisterAttributeString('Legionellenfehler', '');
|
||||
|
||||
$this->RegisterTimer(
|
||||
'Regelzyklus',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Regelzyklus', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'Meldezyklus',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RueckmeldungVerzoegert',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
$this->initialisiereTemperaturen();
|
||||
$this->aktualisiereSichtbarkeit();
|
||||
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetTimerInterval('Regelzyklus', 0);
|
||||
$this->SetTimerInterval('Meldezyklus', 0);
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
$this->SetValue('Verfuegbar', false);
|
||||
$this->SetValue('AenderungMoeglich', false);
|
||||
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registriereFuehlermeldung();
|
||||
$this->SetTimerInterval('Regelzyklus', $this->ReadPropertyInteger('Interval') * 1000);
|
||||
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
||||
if ($this->ReadAttributeInteger('LetzterLegionellenabschluss') <= 0) {
|
||||
$this->WriteAttributeInteger('LetzterLegionellenabschluss', time());
|
||||
}
|
||||
|
||||
$this->regelzyklus(true);
|
||||
}
|
||||
|
||||
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
||||
{
|
||||
if ((int) $senderID === $this->ReadPropertyInteger('Boilerfuehler_PT1')
|
||||
&& (int) $nachricht === self::VM_UPDATE
|
||||
) {
|
||||
$this->regelzyklus(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'Aktiv':
|
||||
$this->SetValue('Aktiv', (bool) $wert);
|
||||
$this->regelzyklus(true);
|
||||
return;
|
||||
|
||||
case 'Mindesttemperatur':
|
||||
case 'Maximaltemperatur':
|
||||
case 'Legionellentemperatur':
|
||||
$this->setzeTemperaturSollwert((string) $ident, $wert);
|
||||
return;
|
||||
|
||||
case 'Regelzyklus':
|
||||
$this->regelzyklus(true);
|
||||
return;
|
||||
|
||||
case 'Melden':
|
||||
if ((bool) $wert) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
}
|
||||
$this->sendeVerbraucherdaten();
|
||||
return;
|
||||
|
||||
case 'ManagerdatenEmpfangen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen als JSON uebergeben werden.');
|
||||
}
|
||||
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new InvalidArgumentException('Managerdaten muessen ein JSON-Objekt sein.');
|
||||
}
|
||||
$this->ManagerdatenEmpfangen($daten);
|
||||
return;
|
||||
|
||||
case 'LogStatus':
|
||||
$this->protokolliereStatus(true);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
public function ManagerdatenEmpfangen(array $daten): void
|
||||
{
|
||||
Nachrichtenvertrag::pruefeManagerdaten($daten);
|
||||
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
|
||||
throw new InvalidArgumentException('Managerdaten sind an eine andere Instanz adressiert.');
|
||||
}
|
||||
|
||||
$managerID = $daten['Kopf']['AbsenderID'];
|
||||
if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) {
|
||||
throw new InvalidArgumentException('Der Manager hat diesen Verbraucher nicht aktiv zugeordnet.');
|
||||
}
|
||||
|
||||
$this->regelzyklus(false);
|
||||
$angebot = $this->leseLeistungsangebot();
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if (!in_array($sollleistung, $angebot, true)) {
|
||||
throw new InvalidArgumentException('Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.');
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||
$this->SetValue('Sollleistung', $sollleistung);
|
||||
$this->SetValue('SollwertGueltig', true);
|
||||
$this->regelzyklus(false);
|
||||
|
||||
// Verhindert eine synchrone Manager-Verbraucher-Endlosschleife.
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
|
||||
private function regelzyklus(bool $meldungPlanen): void
|
||||
{
|
||||
try {
|
||||
$jetzt = time();
|
||||
$this->aktualisiereEnergie($jetzt);
|
||||
$temperaturGueltig = $this->aktualisiereTemperatur();
|
||||
$this->aktualisiereLegionellenstatus($temperaturGueltig, $jetzt);
|
||||
|
||||
[$angebot, $aenderungMoeglich, $verfuegbar, $zwangsleistung] =
|
||||
$this->bestimmeLeistungsangebot($temperaturGueltig, $jetzt);
|
||||
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
||||
$this->SetValue('Verfuegbar', $verfuegbar);
|
||||
$this->SetValue('AenderungMoeglich', $aenderungMoeglich);
|
||||
|
||||
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
||||
if ($letzteVorgabe <= 0
|
||||
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
|
||||
) {
|
||||
$this->SetValue('SollwertGueltig', false);
|
||||
}
|
||||
|
||||
$ziel = 0;
|
||||
if ($verfuegbar) {
|
||||
if ($zwangsleistung !== null) {
|
||||
$ziel = $zwangsleistung;
|
||||
} elseif ((bool) $this->GetValue('SollwertGueltig')) {
|
||||
$ziel = (int) $this->GetValue('Sollleistung');
|
||||
if (!in_array($ziel, $angebot, true)) {
|
||||
$this->SetValue('SollwertGueltig', false);
|
||||
$ziel = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->schalteLeistung($ziel);
|
||||
$this->SetValue('Sollleistung', $ziel);
|
||||
$this->aktualisiereStoerungen($temperaturGueltig);
|
||||
if ($meldungPlanen) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
||||
$this->SetValue('Verfuegbar', false);
|
||||
$this->SetValue('AenderungMoeglich', false);
|
||||
$this->SetValue('Stoerung', true);
|
||||
$this->SetValue('Stoertext', $fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||
$this->protokolliere('Regelzyklus', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function aktualisiereTemperatur(): bool
|
||||
{
|
||||
$fuehlerID = $this->ReadPropertyInteger('Boilerfuehler_PT1');
|
||||
if ($fuehlerID <= 0 || !IPS_VariableExists($fuehlerID)) {
|
||||
$this->SetValue('TemperaturGueltig', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
$variable = IPS_GetVariable($fuehlerID);
|
||||
if (time() - (int) $variable['VariableUpdated'] > $this->ReadPropertyInteger('TemperaturMaxAlter')) {
|
||||
$this->SetValue('TemperaturGueltig', false);
|
||||
return false;
|
||||
}
|
||||
$rohwert = GetValue($fuehlerID);
|
||||
if (!is_int($rohwert) && !is_float($rohwert)) {
|
||||
$this->SetValue('TemperaturGueltig', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
$temperatur = (float) $rohwert;
|
||||
if ($this->ReadPropertyBoolean('BoilertemperaturGlaetten')) {
|
||||
if (!$this->ReadAttributeBoolean('GlaettungInitialisiert')) {
|
||||
$this->WriteAttributeFloat('Glaettungswert', $temperatur);
|
||||
$this->WriteAttributeBoolean('GlaettungInitialisiert', true);
|
||||
} else {
|
||||
$temperatur = WarmwasserRegler::pt1(
|
||||
$this->ReadAttributeFloat('Glaettungswert'),
|
||||
$temperatur,
|
||||
$this->ReadPropertyInteger('Interval'),
|
||||
$this->ReadPropertyInteger('ZeitKonstante')
|
||||
);
|
||||
$this->WriteAttributeFloat('Glaettungswert', $temperatur);
|
||||
}
|
||||
} else {
|
||||
$this->WriteAttributeBoolean('GlaettungInitialisiert', false);
|
||||
}
|
||||
|
||||
$this->SetValue('Boilertemperatur', $temperatur);
|
||||
$this->SetValue('TemperaturGueltig', true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: list<int>, 1: bool, 2: bool, 3: int|null}
|
||||
*/
|
||||
private function bestimmeLeistungsangebot(bool $temperaturGueltig, int $jetzt): array
|
||||
{
|
||||
$stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen'));
|
||||
if (!(bool) $this->GetValue('Aktiv') || !$temperaturGueltig || $stufen === []) {
|
||||
$this->SetValue('NachladenAktiv', false);
|
||||
return [[], false, false, 0];
|
||||
}
|
||||
|
||||
$alleLeistungen = WarmwasserRegler::leistungswerte($stufen);
|
||||
$maximalleistung = max($alleLeistungen);
|
||||
$temperatur = (float) $this->GetValue('Boilertemperatur');
|
||||
$minimum = (float) $this->GetValue('Mindesttemperatur');
|
||||
$maximum = (float) $this->GetValue('Maximaltemperatur');
|
||||
$hysterese = $this->ReadPropertyFloat('Hysterese');
|
||||
$aktuelleLeistung = (int) round((float) $this->GetValue('Istleistung'));
|
||||
|
||||
$nachladen = (bool) $this->GetValue('NachladenAktiv');
|
||||
if ($temperatur < $minimum) {
|
||||
$nachladen = true;
|
||||
} elseif ($temperatur >= $minimum + $hysterese) {
|
||||
$nachladen = false;
|
||||
}
|
||||
$this->SetValue('NachladenAktiv', $nachladen);
|
||||
|
||||
$zeitplanErfordertHeizen = false;
|
||||
$naechster = WarmwasserRegler::naechsterZeitplaneintrag(
|
||||
$this->ReadPropertyString('Zeitplan'),
|
||||
new DateTimeImmutable('now')
|
||||
);
|
||||
if ($naechster !== null && $temperatur < $naechster['Solltemperatur']) {
|
||||
$zeitplanErfordertHeizen = !WarmwasserRegler::kannZielErreichen(
|
||||
(float) $this->ReadPropertyInteger('Boilervolumen'),
|
||||
$temperatur,
|
||||
$naechster['Solltemperatur'],
|
||||
max(0, $naechster['Zeitpunkt'] - $jetzt),
|
||||
$maximalleistung
|
||||
);
|
||||
}
|
||||
|
||||
if ((bool) $this->GetValue('LegionellenbetriebAktiv') || $nachladen || $zeitplanErfordertHeizen) {
|
||||
return [[$maximalleistung], false, true, $maximalleistung];
|
||||
}
|
||||
if ($temperatur >= $maximum) {
|
||||
return [[0], false, true, 0];
|
||||
}
|
||||
if ($aktuelleLeistung > 0 && $temperatur >= $maximum - $hysterese) {
|
||||
return [[$aktuelleLeistung], false, true, $aktuelleLeistung];
|
||||
}
|
||||
|
||||
return [$alleLeistungen, true, true, null];
|
||||
}
|
||||
|
||||
private function aktualisiereLegionellenstatus(bool $temperaturGueltig, int $jetzt): void
|
||||
{
|
||||
$letzterAbschluss = $this->ReadAttributeInteger('LetzterLegionellenabschluss');
|
||||
$this->SetValue('LegioCounter', max(0, $jetzt - $letzterAbschluss));
|
||||
|
||||
if (!$this->ReadPropertyBoolean('LegionellenfunktionAktiv')) {
|
||||
$this->SetValue('LegionellenbetriebAktiv', false);
|
||||
$this->WriteAttributeInteger('LegionellenStart', 0);
|
||||
$this->WriteAttributeInteger('LegionellenSeitZiel', 0);
|
||||
$this->WriteAttributeString('Legionellenfehler', '');
|
||||
return;
|
||||
}
|
||||
|
||||
$aktiv = (bool) $this->GetValue('LegionellenbetriebAktiv');
|
||||
$faellig = $jetzt - $letzterAbschluss >= $this->ReadPropertyInteger('LegionellenIntervallTage') * 86400;
|
||||
if (!$aktiv && $faellig && $this->istNachtstunde((int) date('G', $jetzt))) {
|
||||
$aktiv = true;
|
||||
$this->WriteAttributeInteger('LegionellenStart', $jetzt);
|
||||
$this->WriteAttributeInteger('LegionellenSeitZiel', 0);
|
||||
}
|
||||
|
||||
if ($aktiv && $jetzt - $this->ReadAttributeInteger('LegionellenStart')
|
||||
> $this->ReadPropertyInteger('LegionellenMaximaldauer')
|
||||
) {
|
||||
$aktiv = false;
|
||||
$this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt);
|
||||
$this->WriteAttributeString(
|
||||
'Legionellenfehler',
|
||||
'Legionellenprogramm innerhalb der Maximaldauer nicht abgeschlossen.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($aktiv && $temperaturGueltig
|
||||
&& (float) $this->GetValue('Boilertemperatur') >= (float) $this->GetValue('Legionellentemperatur')
|
||||
) {
|
||||
$seitZiel = $this->ReadAttributeInteger('LegionellenSeitZiel');
|
||||
if ($seitZiel <= 0) {
|
||||
$seitZiel = $jetzt;
|
||||
$this->WriteAttributeInteger('LegionellenSeitZiel', $seitZiel);
|
||||
}
|
||||
if ($jetzt - $seitZiel >= $this->ReadPropertyInteger('LegionellenHaltezeit')) {
|
||||
$aktiv = false;
|
||||
$this->WriteAttributeInteger('LetzterLegionellenabschluss', $jetzt);
|
||||
$this->WriteAttributeInteger('LegionellenStart', 0);
|
||||
$this->WriteAttributeInteger('LegionellenSeitZiel', 0);
|
||||
$this->WriteAttributeString('Legionellenfehler', '');
|
||||
}
|
||||
}
|
||||
|
||||
$this->SetValue('LegionellenbetriebAktiv', $aktiv);
|
||||
}
|
||||
|
||||
private function schalteLeistung(int $zielLeistung): void
|
||||
{
|
||||
$stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen'));
|
||||
$zielStufe = WarmwasserRegler::stufeFuerLeistung($stufen, $zielLeistung);
|
||||
$bisherigeLeistung = (int) round((float) $this->GetValue('Istleistung'));
|
||||
if ($zielLeistung === $bisherigeLeistung && $this->kontakteEntsprechenZiel($stufen, $zielLeistung)) {
|
||||
$this->WriteAttributeString('Schaltfehler', '');
|
||||
$this->verarbeiteIdle(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Break-before-make: zuerst alle Kontakte aus, danach genau eine Stufe ein.
|
||||
foreach ($stufen as $stufe) {
|
||||
RequestAction($stufe['KontaktID'], false);
|
||||
}
|
||||
if ($zielLeistung > 0) {
|
||||
foreach ($stufen as $stufe) {
|
||||
if ($stufe['Leistung_W'] === $zielLeistung) {
|
||||
RequestAction($stufe['KontaktID'], true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $fehler) {
|
||||
foreach ($stufen as $stufe) {
|
||||
try {
|
||||
RequestAction($stufe['KontaktID'], false);
|
||||
} catch (Throwable $ignoriert) {
|
||||
}
|
||||
}
|
||||
$this->SetValue('Istleistung', 0.0);
|
||||
$this->SetValue('AktiveStufe', 0);
|
||||
throw new RuntimeException('Leistungsstufe konnte nicht sicher geschaltet werden: ' . $fehler->getMessage());
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Schaltfehler', '');
|
||||
$this->SetValue('Istleistung', (float) $zielLeistung);
|
||||
$this->SetValue('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||
$this->SetValue('AktiveStufe', $zielStufe);
|
||||
$this->verarbeiteIdle(true);
|
||||
$this->protokolliere('Leistungsstufe', ['Leistung_W' => $zielLeistung, 'Stufe' => $zielStufe]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{Stufe: int, Leistung_W: int, KontaktID: int}> $stufen
|
||||
*/
|
||||
private function kontakteEntsprechenZiel(array $stufen, int $zielLeistung): bool
|
||||
{
|
||||
foreach ($stufen as $stufe) {
|
||||
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||
return false;
|
||||
}
|
||||
$sollEin = $zielLeistung > 0 && $stufe['Leistung_W'] === $zielLeistung;
|
||||
if ((bool) GetValue($stufe['KontaktID']) !== $sollEin) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function verarbeiteIdle(bool $geaendert): void
|
||||
{
|
||||
$counter = (int) $this->GetValue('IdleCounter');
|
||||
if ($geaendert) {
|
||||
$counter = $this->ReadPropertyInteger('IdleCounterMax');
|
||||
} elseif ($counter > 0) {
|
||||
--$counter;
|
||||
}
|
||||
$this->SetValue('IdleCounter', $counter);
|
||||
$this->SetValue('Idle', $counter === 0);
|
||||
}
|
||||
|
||||
private function aktualisiereEnergie(int $jetzt): void
|
||||
{
|
||||
$letzteBerechnung = $this->ReadAttributeInteger('LetzteBerechnung');
|
||||
$this->WriteAttributeInteger('LetzteBerechnung', $jetzt);
|
||||
if ($letzteBerechnung <= 0 || $jetzt <= $letzteBerechnung) {
|
||||
return;
|
||||
}
|
||||
|
||||
$zuwachs = WarmwasserRegler::energieKWh(
|
||||
(float) $this->GetValue('Istleistung'),
|
||||
$jetzt - $letzteBerechnung
|
||||
);
|
||||
$this->SetValue('BezogeneEnergie', (float) $this->GetValue('BezogeneEnergie') + $zuwachs);
|
||||
}
|
||||
|
||||
private function aktualisiereStoerungen(bool $temperaturGueltig): void
|
||||
{
|
||||
$stoerungen = [];
|
||||
if (!$temperaturGueltig) {
|
||||
$stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.';
|
||||
}
|
||||
$uebertemperatur = $temperaturGueltig
|
||||
&& (float) $this->GetValue('Boilertemperatur')
|
||||
> max(
|
||||
(float) $this->GetValue('Maximaltemperatur'),
|
||||
(float) $this->GetValue('Legionellentemperatur')
|
||||
) + $this->ReadPropertyFloat('Hysterese');
|
||||
if ($uebertemperatur) {
|
||||
$stoerungen[] = 'Uebertemperatur erkannt.';
|
||||
}
|
||||
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||
$stoerungen[] = $this->ReadAttributeString('Schaltfehler');
|
||||
}
|
||||
if ($this->ReadAttributeString('Legionellenfehler') !== '') {
|
||||
$stoerungen[] = $this->ReadAttributeString('Legionellenfehler');
|
||||
}
|
||||
|
||||
$this->SetValue('Stoerung', $stoerungen !== []);
|
||||
$this->SetValue('Stoertext', implode("\n", array_values(array_unique($stoerungen))));
|
||||
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
||||
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
||||
} elseif (!$temperaturGueltig) {
|
||||
$this->SetStatus(self::STATUS_TEMPERATUR_UNGUELTIG);
|
||||
} else {
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeVerbraucherdaten(): void
|
||||
{
|
||||
foreach ($this->zugeordneteManagerIDs() as $managerID) {
|
||||
$daten = $this->baueVerbraucherdaten($managerID);
|
||||
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
|
||||
try {
|
||||
IPS_RequestAction(
|
||||
$managerID,
|
||||
'VerbraucherdatenEmpfangen',
|
||||
json_encode($daten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->protokolliere('Managerkommunikation', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function baueVerbraucherdaten(int $managerID): array
|
||||
{
|
||||
$temperaturGueltig = (bool) $this->GetValue('TemperaturGueltig');
|
||||
$schaltfehler = $this->ReadAttributeString('Schaltfehler');
|
||||
$legionellenfehler = $this->ReadAttributeString('Legionellenfehler');
|
||||
$uebertemperatur = $temperaturGueltig
|
||||
&& (float) $this->GetValue('Boilertemperatur')
|
||||
> max(
|
||||
(float) $this->GetValue('Maximaltemperatur'),
|
||||
(float) $this->GetValue('Legionellentemperatur')
|
||||
) + $this->ReadPropertyFloat('Hysterese');
|
||||
|
||||
return [
|
||||
'Kopf' => [
|
||||
'Version' => Nachrichtenvertrag::VERSION,
|
||||
'AbsenderID' => $this->InstanceID,
|
||||
'EmpfaengerID' => $managerID,
|
||||
'Zeitpunkt' => time(),
|
||||
],
|
||||
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
|
||||
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
|
||||
'Leistungswerte_W' => $this->leseLeistungsangebot(),
|
||||
'AenderungMoeglich' => (bool) $this->GetValue('AenderungMoeglich'),
|
||||
'Verfuegbar' => (bool) $this->GetValue('Verfuegbar'),
|
||||
'Istleistung_W' => (float) $this->GetValue('Istleistung'),
|
||||
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET,
|
||||
'Zustand' => [
|
||||
[
|
||||
'Kennung' => 'Sollleistung_W',
|
||||
'Art' => 'Sollwert',
|
||||
'Wert' => (bool) $this->GetValue('SollwertGueltig')
|
||||
? (int) $this->GetValue('Sollleistung')
|
||||
: null,
|
||||
'Einheit' => 'W',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Wassertemperatur_C',
|
||||
'Art' => 'Istwert',
|
||||
'Wert' => $temperaturGueltig ? (float) $this->GetValue('Boilertemperatur') : null,
|
||||
'Einheit' => 'C',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Maximaltemperatur_C',
|
||||
'Art' => 'Sollwert',
|
||||
'Wert' => (float) $this->GetValue('Maximaltemperatur'),
|
||||
'Einheit' => 'C',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'AktiveStufe',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (int) $this->GetValue('AktiveStufe'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'NachladenAktiv',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->GetValue('NachladenAktiv'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'LegionellenbetriebAktiv',
|
||||
'Art' => 'Status',
|
||||
'Wert' => (bool) $this->GetValue('LegionellenbetriebAktiv'),
|
||||
'Einheit' => '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Fuehlerfehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => !$temperaturGueltig,
|
||||
'Einheit' => '',
|
||||
'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Uebertemperatur',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => $uebertemperatur,
|
||||
'Einheit' => '',
|
||||
'Text' => $uebertemperatur ? 'Uebertemperatur erkannt.' : '',
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Schaltfehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => $schaltfehler !== '',
|
||||
'Einheit' => '',
|
||||
'Text' => $schaltfehler,
|
||||
],
|
||||
[
|
||||
'Kennung' => 'Legionellenfehler',
|
||||
'Art' => 'Stoerung',
|
||||
'Wert' => $legionellenfehler !== '',
|
||||
'Einheit' => '',
|
||||
'Text' => $legionellenfehler,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function zugeordneteManagerIDs(): array
|
||||
{
|
||||
$ergebnis = [];
|
||||
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
|
||||
try {
|
||||
$zuordnung = json_decode(
|
||||
(string) IPS_GetProperty($managerID, 'VerbraucherZuordnung'),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (Throwable $fehler) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($zuordnung)) {
|
||||
continue;
|
||||
}
|
||||
foreach ($zuordnung as $eintrag) {
|
||||
if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) {
|
||||
continue;
|
||||
}
|
||||
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
|
||||
if ($instanzID === $this->InstanceID) {
|
||||
$ergebnis[] = (int) $managerID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($ergebnis));
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) < 0) {
|
||||
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
||||
}
|
||||
}
|
||||
foreach ([
|
||||
'Meldeintervall', 'VorgabeTimeout', 'Interval', 'IdleCounterMax', 'TemperaturMaxAlter',
|
||||
'ZeitKonstante', 'Boilervolumen', 'LegionellenIntervallTage', 'LegionellenMaximaldauer',
|
||||
] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||
}
|
||||
}
|
||||
if ($this->ReadPropertyInteger('LegionellenHaltezeit') < 0) {
|
||||
throw new InvalidArgumentException('LegionellenHaltezeit darf nicht negativ sein.');
|
||||
}
|
||||
if ($this->ReadPropertyFloat('Hysterese') <= 0) {
|
||||
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||
}
|
||||
foreach (['NachtstartStunde', 'NachtendeStunde'] as $property) {
|
||||
$stunde = $this->ReadPropertyInteger($property);
|
||||
if ($stunde < 0 || $stunde > 23) {
|
||||
throw new InvalidArgumentException($property . ' muss zwischen 0 und 23 liegen.');
|
||||
}
|
||||
}
|
||||
|
||||
$minimum = (float) $this->GetValue('Mindesttemperatur');
|
||||
$maximum = (float) $this->GetValue('Maximaltemperatur');
|
||||
$legionellen = (float) $this->GetValue('Legionellentemperatur');
|
||||
if ($minimum < 0 || $minimum >= $maximum || $legionellen < $maximum) {
|
||||
throw new InvalidArgumentException(
|
||||
'Temperaturen muessen Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur erfuellen.'
|
||||
);
|
||||
}
|
||||
|
||||
$fuehlerID = $this->ReadPropertyInteger('Boilerfuehler_PT1');
|
||||
if ($fuehlerID <= 0 || !IPS_VariableExists($fuehlerID)) {
|
||||
throw new InvalidArgumentException('Boilerfuehler_PT1 ist nicht eingerichtet.');
|
||||
}
|
||||
$fuehler = IPS_GetVariable($fuehlerID);
|
||||
if (!in_array((int) $fuehler['VariableType'], [1, 2], true)) {
|
||||
throw new InvalidArgumentException('Boilerfuehler_PT1 muss eine Integer- oder Floatvariable sein.');
|
||||
}
|
||||
|
||||
$stufen = WarmwasserRegler::dekodiereLeistungsstufen($this->ReadPropertyString('LeistungsStufen'));
|
||||
if ($stufen === []) {
|
||||
throw new InvalidArgumentException('Mindestens eine Leistungsstufe ist erforderlich.');
|
||||
}
|
||||
foreach ($stufen as $stufe) {
|
||||
if (!IPS_VariableExists($stufe['KontaktID'])) {
|
||||
throw new InvalidArgumentException('Schaltkontakt ' . $stufe['KontaktID'] . ' existiert nicht.');
|
||||
}
|
||||
$kontakt = IPS_GetVariable($stufe['KontaktID']);
|
||||
if ((int) $kontakt['VariableType'] !== 0) {
|
||||
throw new InvalidArgumentException('Alle Schaltkontakte muessen Booleanvariablen sein.');
|
||||
}
|
||||
}
|
||||
WarmwasserRegler::naechsterZeitplaneintrag(
|
||||
$this->ReadPropertyString('Zeitplan'),
|
||||
new DateTimeImmutable('now')
|
||||
);
|
||||
}
|
||||
|
||||
private function registriereFuehlermeldung(): void
|
||||
{
|
||||
$alt = $this->ReadAttributeInteger('RegistrierteFuehlerID');
|
||||
$neu = $this->ReadPropertyInteger('Boilerfuehler_PT1');
|
||||
if ($alt > 0 && $alt !== $neu) {
|
||||
$this->UnregisterMessage($alt, self::VM_UPDATE);
|
||||
}
|
||||
if ($neu > 0 && $neu !== $alt) {
|
||||
$this->RegisterMessage($neu, self::VM_UPDATE);
|
||||
}
|
||||
$this->WriteAttributeInteger('RegistrierteFuehlerID', $neu);
|
||||
}
|
||||
|
||||
private function initialisiereTemperaturen(): void
|
||||
{
|
||||
if ($this->ReadAttributeBoolean('InitialwerteGesetzt')) {
|
||||
return;
|
||||
}
|
||||
$this->SetValue('Mindesttemperatur', 45.0);
|
||||
$this->SetValue('Maximaltemperatur', 60.0);
|
||||
$this->SetValue('Legionellentemperatur', 65.0);
|
||||
$this->SetValue('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
||||
$this->SetValue('Idle', true);
|
||||
$this->WriteAttributeBoolean('InitialwerteGesetzt', true);
|
||||
}
|
||||
|
||||
/** @param mixed $wert */
|
||||
private function setzeTemperaturSollwert(string $ident, $wert): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||
throw new RuntimeException('Temperatureinstellungen sind in der Visualisierung nicht freigegeben.');
|
||||
}
|
||||
if (!is_int($wert) && !is_float($wert)) {
|
||||
throw new InvalidArgumentException('Temperatursollwert muss numerisch sein.');
|
||||
}
|
||||
$temperatur = (float) $wert;
|
||||
if ($temperatur < 0 || $temperatur > 100) {
|
||||
throw new InvalidArgumentException('Temperatursollwert muss zwischen 0 und 100 Grad C liegen.');
|
||||
}
|
||||
|
||||
$werte = [
|
||||
'Mindesttemperatur' => (float) $this->GetValue('Mindesttemperatur'),
|
||||
'Maximaltemperatur' => (float) $this->GetValue('Maximaltemperatur'),
|
||||
'Legionellentemperatur' => (float) $this->GetValue('Legionellentemperatur'),
|
||||
];
|
||||
$werte[$ident] = $temperatur;
|
||||
if ($werte['Mindesttemperatur'] >= $werte['Maximaltemperatur']
|
||||
|| $werte['Maximaltemperatur'] > $werte['Legionellentemperatur']
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
'Temperaturen muessen Mindesttemperatur < Maximaltemperatur <= Legionellentemperatur erfuellen.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->SetValue($ident, $temperatur);
|
||||
$this->regelzyklus(true);
|
||||
}
|
||||
|
||||
private function aktualisiereSichtbarkeit(): void
|
||||
{
|
||||
$diagnose = $this->ReadPropertyBoolean('DiagnosevariablenAnzeigen');
|
||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||
IPS_SetHidden($this->GetIDForIdent($ident), !$diagnose);
|
||||
}
|
||||
|
||||
$einstellungen = $this->ReadPropertyBoolean('EinstellungenInVisu');
|
||||
foreach (['Mindesttemperatur', 'Maximaltemperatur', 'Legionellentemperatur'] as $ident) {
|
||||
IPS_SetHidden($this->GetIDForIdent($ident), !$einstellungen);
|
||||
if ($einstellungen) {
|
||||
$this->EnableAction($ident);
|
||||
} else {
|
||||
$this->DisableAction($ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
private function leseLeistungsangebot(): array
|
||||
{
|
||||
$angebot = json_decode($this->ReadAttributeString('Leistungsangebot'), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($angebot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_map('intval', $angebot));
|
||||
}
|
||||
|
||||
private function istNachtstunde(int $stunde): bool
|
||||
{
|
||||
$start = $this->ReadPropertyInteger('NachtstartStunde');
|
||||
$ende = $this->ReadPropertyInteger('NachtendeStunde');
|
||||
if ($start === $ende) {
|
||||
return true;
|
||||
}
|
||||
if ($start < $ende) {
|
||||
return $stunde >= $start && $stunde < $ende;
|
||||
}
|
||||
|
||||
return $stunde >= $start || $stunde < $ende;
|
||||
}
|
||||
|
||||
private function protokolliereStatus(bool $erzwingen): void
|
||||
{
|
||||
if (!$erzwingen && !$this->ReadPropertyBoolean('LoggingEin')) {
|
||||
return;
|
||||
}
|
||||
$this->SendDebug('Status', json_encode([
|
||||
'Aktiv' => (bool) $this->GetValue('Aktiv'),
|
||||
'Boilertemperatur_C' => (float) $this->GetValue('Boilertemperatur'),
|
||||
'TemperaturGueltig' => (bool) $this->GetValue('TemperaturGueltig'),
|
||||
'Leistungsangebot_W' => $this->leseLeistungsangebot(),
|
||||
'Sollleistung_W' => (int) $this->GetValue('Sollleistung'),
|
||||
'Istleistung_W' => (float) $this->GetValue('Istleistung'),
|
||||
'AktiveStufe' => (int) $this->GetValue('AktiveStufe'),
|
||||
'Stoertext' => (string) $this->GetValue('Stoertext'),
|
||||
], JSON_THROW_ON_ERROR), 0);
|
||||
}
|
||||
|
||||
/** @param mixed $daten */
|
||||
private function protokolliere(string $bezeichnung, $daten): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||
return;
|
||||
}
|
||||
$this->SendDebug(
|
||||
$bezeichnung,
|
||||
is_string($daten) ? $daten : json_encode($daten, JSON_THROW_ON_ERROR),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user