964 lines
38 KiB
PHP
964 lines
38 KiB
PHP
<?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/EinStufigRegler.php';
|
|
|
|
use Belevo\EnelixEMS\EinStufigRegler;
|
|
use Belevo\EnelixEMS\Nachrichtenvertrag;
|
|
use Belevo\EnelixEMS\VerbraucherBasisTrait;
|
|
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
|
|
|
|
class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
|
|
{
|
|
use VerbraucherBasisTrait;
|
|
|
|
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
|
|
private const STATUS_AKTIV = 102;
|
|
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
|
|
private const STATUS_SCHALTFEHLER = 202;
|
|
private const VM_UPDATE = 10603;
|
|
|
|
/** @var list<string> */
|
|
private const DIAGNOSE_VARIABLEN = [
|
|
'Istleistung',
|
|
'Leistungsquelle',
|
|
'Sollleistung',
|
|
'SollwertGueltig',
|
|
'Verfuegbar',
|
|
'AenderungMoeglich',
|
|
'Stoerung',
|
|
'Stoertext',
|
|
'Rueckmeldefehler',
|
|
'SchaltbefehlAusstehend',
|
|
'RestMindestzeit',
|
|
];
|
|
|
|
public function Create(): void
|
|
{
|
|
parent::Create();
|
|
|
|
$this->registriereVerbraucherBasis();
|
|
$this->RegisterPropertyInteger('Nennleistung', 0);
|
|
$this->RegisterPropertyInteger('SchaltkontaktVariableID', 0);
|
|
$this->RegisterPropertyBoolean('SchaltkontaktInvertiert', false);
|
|
$this->RegisterPropertyInteger('RueckmeldungVariableID', 0);
|
|
$this->RegisterPropertyInteger('Mindestlaufzeit', 0);
|
|
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 5);
|
|
$this->RegisterPropertyInteger('Mindestausschaltdauer', 5);
|
|
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
|
|
|
$this->RegisterVariableBoolean('Schaltzustand', 'Schaltzustand', '~Switch', 100);
|
|
$this->RegisterVariableInteger('Tageslaufzeit', 'Tageslaufzeit', '', 110);
|
|
|
|
$this->RegisterAttributeInteger('RegistrierterSchaltkontakt', 0);
|
|
$this->RegisterAttributeInteger('RegistrierteRueckmeldung', 0);
|
|
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
|
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
|
$this->RegisterAttributeInteger('BestaetigtSeit', 0);
|
|
$this->RegisterAttributeInteger('FreigabeAb', 0);
|
|
$this->RegisterAttributeInteger('LaufzeitStandZeit', 0);
|
|
$this->RegisterAttributeString('LaufzeitTag', '');
|
|
$this->RegisterAttributeString('Leistungsangebot', '[0]');
|
|
$this->RegisterAttributeBoolean('Initialisiert', false);
|
|
$this->RegisterAttributeBoolean('SchaltvorgangLaeuft', false);
|
|
$this->RegisterAttributeBoolean('SchaltpruefungAusstehend', false);
|
|
$this->RegisterAttributeBoolean('Schaltziel', false);
|
|
$this->RegisterAttributeString('Schaltfehler', '');
|
|
|
|
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
|
|
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
|
|
$this->RegisterAttributeBoolean('ZustandSollwertGueltig', false);
|
|
$this->RegisterAttributeBoolean('ZustandVerfuegbar', false);
|
|
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
|
|
$this->RegisterAttributeBoolean('ZustandStoerung', false);
|
|
$this->RegisterAttributeString('ZustandStoertext', '');
|
|
$this->RegisterAttributeBoolean('ZustandSchaltzustand', false);
|
|
$this->RegisterAttributeInteger('ZustandTageslaufzeit', 0);
|
|
$this->RegisterAttributeBoolean('ZustandRueckmeldefehler', false);
|
|
$this->RegisterAttributeBoolean('ZustandSchaltbefehlAusstehend', false);
|
|
$this->RegisterAttributeInteger('ZustandRestMindestzeit', 0);
|
|
|
|
$this->RegisterTimer(
|
|
'Meldezyklus',
|
|
0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
|
);
|
|
$this->RegisterTimer(
|
|
'RueckmeldungVerzoegert',
|
|
0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
|
);
|
|
// Bestehende Instanzen behalten den alten Timer deaktiviert.
|
|
$this->RegisterTimer('Umschaltfreigabe', 0, '');
|
|
$this->RegisterTimer(
|
|
'MindestzeitAbgelaufen',
|
|
0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'MindestzeitAbgelaufen', 0);"
|
|
);
|
|
$this->RegisterTimer(
|
|
'VorgabeTimeout',
|
|
0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', 0);"
|
|
);
|
|
$this->RegisterTimer(
|
|
'Tagesplanung',
|
|
0,
|
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Tagesplanung', 0);"
|
|
);
|
|
}
|
|
|
|
public function ApplyChanges(): void
|
|
{
|
|
parent::ApplyChanges();
|
|
|
|
$this->aktualisiereVariablen();
|
|
$this->registriereMeldungen();
|
|
|
|
try {
|
|
$this->pruefeKonfiguration();
|
|
} catch (Throwable $fehler) {
|
|
$this->deaktiviereTimer();
|
|
$this->setzeZustand('Verfuegbar', false);
|
|
$this->setzeZustand('AenderungMoeglich', false);
|
|
$this->setzeStoerung($fehler->getMessage(), false);
|
|
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
|
$this->protokolliere('Konfiguration', $fehler->getMessage());
|
|
return;
|
|
}
|
|
|
|
$this->SetTimerInterval('Meldezyklus', $this->ReadPropertyInteger('Meldeintervall') * 1000);
|
|
$this->initialisiereZustand();
|
|
$this->aktualisiere(true);
|
|
}
|
|
|
|
public function MessageSink($zeitstempel, $senderID, $nachricht, $daten): void
|
|
{
|
|
if ((int) $nachricht !== self::VM_UPDATE || $this->ReadAttributeBoolean('SchaltvorgangLaeuft')) {
|
|
return;
|
|
}
|
|
|
|
$relevanteIDs = array_filter([
|
|
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
|
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
|
]);
|
|
if (in_array((int) $senderID, $relevanteIDs, true)) {
|
|
$this->aktualisiere(true);
|
|
}
|
|
}
|
|
|
|
public function RequestAction($ident, $wert): void
|
|
{
|
|
switch ($ident) {
|
|
case 'Aktiv':
|
|
$this->SetValue('Aktiv', (bool) $wert);
|
|
if (!(bool) $wert) {
|
|
$this->setzeZustand('SollwertGueltig', false);
|
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
|
}
|
|
$this->aktualisiere(true);
|
|
return;
|
|
|
|
case 'Melden':
|
|
if ((bool) $wert) {
|
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
|
}
|
|
$this->aktualisiere(false);
|
|
$this->sendeVerbraucherdaten();
|
|
return;
|
|
|
|
case 'MindestzeitAbgelaufen':
|
|
case 'VorgabeTimeout':
|
|
case 'Tagesplanung':
|
|
$this->SetTimerInterval((string) $ident, 0);
|
|
$this->aktualisiere(true);
|
|
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;
|
|
}
|
|
|
|
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->aktualisiere(false);
|
|
$sollleistung = $daten['Sollleistung_W'];
|
|
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
|
|
throw new InvalidArgumentException(
|
|
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
|
|
);
|
|
}
|
|
|
|
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
|
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
|
$this->setzeZustand('Sollleistung', $sollleistung);
|
|
$this->setzeZustand('SollwertGueltig', true);
|
|
$this->planeVorgabeTimeout(time());
|
|
$this->aktualisiere(false);
|
|
$this->planeMeldung();
|
|
}
|
|
|
|
private function aktualisiere(bool $meldungPlanen): void
|
|
{
|
|
try {
|
|
$jetzt = time();
|
|
$this->aktualisiereTageslaufzeit($jetzt);
|
|
$vorherigerSchaltzustand = (bool) $this->leseZustand('Schaltzustand');
|
|
$schaltzustand = $this->leseSchaltzustand();
|
|
|
|
if ($schaltzustand !== $vorherigerSchaltzustand) {
|
|
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
|
}
|
|
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
|
|
$this->pruefeVorgabeTimeout($jetzt);
|
|
if ((bool) $this->leseZustand('SollwertGueltig')
|
|
&& !in_array(
|
|
(int) $this->leseZustand('Sollleistung'),
|
|
[0, $this->ReadPropertyInteger('Nennleistung')],
|
|
true
|
|
)
|
|
) {
|
|
$this->setzeZustand('SollwertGueltig', false);
|
|
}
|
|
|
|
$aktiv = (bool) $this->GetValue('Aktiv');
|
|
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
|
$rueckmeldefehler = $this->ReadPropertyInteger('RueckmeldungVariableID') > 0
|
|
&& !$wartetAufRueckmeldung
|
|
&& $schaltzustand !== $this->leseAktorSchaltzustand();
|
|
$this->setzeZustand('Rueckmeldefehler', $rueckmeldefehler);
|
|
|
|
$fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
|
|
&& !$rueckmeldefehler;
|
|
$verfuegbar = $aktiv && $fehlerfrei;
|
|
$mindestlaufzeitErzwungen = $verfuegbar
|
|
&& $this->mussMindestlaufzeitErzwingen($jetzt);
|
|
$restMindestzeit = $wartetAufRueckmeldung
|
|
? 0
|
|
: EinStufigRegler::restMindestzeit(
|
|
$this->ReadAttributeInteger('FreigabeAb'),
|
|
$jetzt
|
|
);
|
|
|
|
$ziel = 0;
|
|
if ($verfuegbar) {
|
|
if ($mindestlaufzeitErzwungen) {
|
|
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
|
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
|
$ziel = (int) $this->leseZustand('Sollleistung');
|
|
}
|
|
}
|
|
|
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
|
$zielzustand = $ziel > 0;
|
|
$befehlNoetig = $ziel !== $istleistung
|
|
|| ($wartetAufRueckmeldung
|
|
&& $zielzustand !== $this->ReadAttributeBoolean('Schaltziel'));
|
|
// Die lokale Deaktivierung bleibt der einzige bewusste Mindestzeit-Override.
|
|
$sicheresAusschalten = !$zielzustand && !$aktiv;
|
|
if ($befehlNoetig
|
|
&& ($sicheresAusschalten
|
|
|| ($restMindestzeit === 0 && !$wartetAufRueckmeldung && $fehlerfrei))
|
|
) {
|
|
$this->schalte($zielzustand, $jetzt);
|
|
$schaltzustand = $this->leseSchaltzustand();
|
|
$this->pruefeAusstehendenSchaltvorgang($schaltzustand, $jetzt);
|
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
|
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
|
$restMindestzeit = $wartetAufRueckmeldung
|
|
? 0
|
|
: EinStufigRegler::restMindestzeit(
|
|
$this->ReadAttributeInteger('FreigabeAb'),
|
|
$jetzt
|
|
);
|
|
}
|
|
|
|
$this->setzeZustand('Sollleistung', $ziel);
|
|
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
|
$this->setzeZustand('SchaltbefehlAusstehend', $wartetAufRueckmeldung);
|
|
$this->setzeZustand('RestMindestzeit', $restMindestzeit);
|
|
$aenderungMoeglich = $verfuegbar
|
|
&& !$mindestlaufzeitErzwungen
|
|
&& $restMindestzeit === 0
|
|
&& !$wartetAufRueckmeldung;
|
|
$this->setzeZustand('AenderungMoeglich', $aenderungMoeglich);
|
|
|
|
$angebot = EinStufigRegler::leistungsangebot(
|
|
$this->ReadPropertyInteger('Nennleistung'),
|
|
$istleistung,
|
|
$verfuegbar,
|
|
$aenderungMoeglich,
|
|
$mindestlaufzeitErzwungen
|
|
);
|
|
$this->WriteAttributeString('Leistungsangebot', json_encode($angebot, JSON_THROW_ON_ERROR));
|
|
$this->aktualisiereStoerung();
|
|
$this->planeTimer($jetzt);
|
|
$this->SetStatus(
|
|
$this->ReadAttributeString('Schaltfehler') === ''
|
|
&& !(bool) $this->leseZustand('Rueckmeldefehler')
|
|
? self::STATUS_AKTIV
|
|
: self::STATUS_SCHALTFEHLER
|
|
);
|
|
|
|
if ($meldungPlanen) {
|
|
$this->planeMeldung();
|
|
}
|
|
} catch (Throwable $fehler) {
|
|
$this->WriteAttributeString('Schaltfehler', $fehler->getMessage());
|
|
$this->setzeZustand('Verfuegbar', false);
|
|
$this->setzeZustand('AenderungMoeglich', false);
|
|
$this->setzeStoerung($fehler->getMessage(), false);
|
|
$this->SetStatus(self::STATUS_SCHALTFEHLER);
|
|
$this->protokolliere('Aktualisierung', $fehler->getMessage());
|
|
}
|
|
}
|
|
|
|
private function schalte(bool $einschalten, int $jetzt): void
|
|
{
|
|
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
|
$aktorwert = $einschalten !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
|
|
|
$this->WriteAttributeBoolean('Schaltziel', $einschalten);
|
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', true);
|
|
$this->setzeZustand('SchaltbefehlAusstehend', true);
|
|
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
|
|
try {
|
|
RequestAction($kontaktID, $aktorwert);
|
|
} catch (Throwable $fehler) {
|
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
|
$this->setzeZustand('SchaltbefehlAusstehend', false);
|
|
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
|
|
} finally {
|
|
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
|
|
}
|
|
|
|
$istzustand = $this->leseSchaltzustand();
|
|
if ($istzustand === $einschalten) {
|
|
$this->bestaetigeZustandswechsel($istzustand, $jetzt);
|
|
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
|
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
|
$this->setzeZustand('SchaltbefehlAusstehend', false);
|
|
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
|
|
} else {
|
|
// Die physische Rueckmeldung bestimmt Zustand und Beginn der Mindestzeit.
|
|
$this->setzeZustand('Rueckmeldefehler', false);
|
|
}
|
|
|
|
$this->WriteAttributeString('Schaltfehler', '');
|
|
$this->protokolliere('Schaltbefehl', ['Eingeschaltet' => $einschalten]);
|
|
}
|
|
|
|
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
|
|
{
|
|
if ($this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
|
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
|
|
) {
|
|
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
|
}
|
|
|
|
$this->setzeZustand(
|
|
'SchaltbefehlAusstehend',
|
|
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
|
);
|
|
}
|
|
|
|
private function bestaetigeZustandswechsel(bool $schaltzustand, int $jetzt): void
|
|
{
|
|
$vorherigerZustand = (bool) $this->leseZustand('Schaltzustand');
|
|
$zustandGeaendert = $schaltzustand !== $vorherigerZustand
|
|
|| $this->ReadAttributeInteger('BestaetigtSeit') <= 0;
|
|
|
|
$this->setzeSchaltzustand($schaltzustand);
|
|
if ($zustandGeaendert) {
|
|
$this->WriteAttributeInteger('BestaetigtSeit', $jetzt);
|
|
$this->WriteAttributeInteger(
|
|
'FreigabeAb',
|
|
EinStufigRegler::freigabezeitpunkt(
|
|
$schaltzustand,
|
|
$jetzt,
|
|
$this->ReadPropertyInteger('Mindesteinschaltdauer'),
|
|
$this->ReadPropertyInteger('Mindestausschaltdauer')
|
|
)
|
|
);
|
|
}
|
|
|
|
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
|
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
|
}
|
|
$this->setzeZustand(
|
|
'SchaltbefehlAusstehend',
|
|
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
|
);
|
|
$this->setzeZustand('Rueckmeldefehler', false);
|
|
$this->protokolliere('Schaltzustand bestaetigt', ['Eingeschaltet' => $schaltzustand]);
|
|
}
|
|
|
|
private function aktualisiereTageslaufzeit(int $jetzt): void
|
|
{
|
|
$werte = EinStufigRegler::aktualisiereTageslaufzeit(
|
|
$this->ReadAttributeString('LaufzeitTag'),
|
|
(int) $this->leseZustand('Tageslaufzeit'),
|
|
$this->ReadAttributeInteger('LaufzeitStandZeit'),
|
|
$jetzt,
|
|
(bool) $this->leseZustand('Schaltzustand'),
|
|
$this->zeitzone()
|
|
);
|
|
|
|
$this->WriteAttributeString('LaufzeitTag', $werte['Tag']);
|
|
$this->WriteAttributeInteger('LaufzeitStandZeit', $werte['StandZeit']);
|
|
$this->setzeZustand('Tageslaufzeit', $werte['Sekunden']);
|
|
}
|
|
|
|
private function mussMindestlaufzeitErzwingen(int $jetzt): bool
|
|
{
|
|
return EinStufigRegler::mussMindestlaufzeitErzwingen(
|
|
(int) $this->leseZustand('Tageslaufzeit'),
|
|
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
|
EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone())
|
|
);
|
|
}
|
|
|
|
private function pruefeVorgabeTimeout(int $jetzt): void
|
|
{
|
|
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
|
return;
|
|
}
|
|
|
|
$letzteVorgabe = $this->ReadAttributeInteger('LetzteVorgabeZeit');
|
|
if ($letzteVorgabe <= 0
|
|
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
|
|
) {
|
|
$this->setzeZustand('SollwertGueltig', false);
|
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
|
}
|
|
}
|
|
|
|
private function planeTimer(int $jetzt): void
|
|
{
|
|
$this->SetTimerInterval('Umschaltfreigabe', 0);
|
|
$freigabeAb = $this->ReadAttributeInteger('FreigabeAb');
|
|
$wartetAufRueckmeldung = $this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
|
$this->SetTimerInterval(
|
|
'MindestzeitAbgelaufen',
|
|
!$wartetAufRueckmeldung && $freigabeAb > $jetzt
|
|
? max(1, ($freigabeAb - $jetzt) * 1000)
|
|
: 0
|
|
);
|
|
$this->planeVorgabeTimeout($jetzt);
|
|
|
|
$bisTagesende = EinStufigRegler::sekundenBisTagesende($jetzt, $this->zeitzone());
|
|
$naechstePruefung = EinStufigRegler::sekundenBisNaechsteTagespruefung(
|
|
(int) $this->leseZustand('Tageslaufzeit'),
|
|
$this->ReadPropertyInteger('Mindestlaufzeit'),
|
|
$bisTagesende,
|
|
(bool) $this->leseZustand('Schaltzustand')
|
|
);
|
|
$this->SetTimerInterval('Tagesplanung', $naechstePruefung * 1000);
|
|
}
|
|
|
|
private function planeVorgabeTimeout(int $jetzt): void
|
|
{
|
|
if (!(bool) $this->leseZustand('SollwertGueltig')) {
|
|
$this->SetTimerInterval('VorgabeTimeout', 0);
|
|
return;
|
|
}
|
|
|
|
$ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit')
|
|
+ $this->ReadPropertyInteger('VorgabeTimeout');
|
|
$this->SetTimerInterval('VorgabeTimeout', max(1, ($ablauf - $jetzt) * 1000));
|
|
}
|
|
|
|
private function planeMeldung(): void
|
|
{
|
|
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
|
}
|
|
|
|
private function initialisiereZustand(): void
|
|
{
|
|
$jetzt = time();
|
|
$schaltzustand = $this->leseSchaltzustand();
|
|
|
|
if (!$this->ReadAttributeBoolean('Initialisiert')) {
|
|
$this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt));
|
|
$this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt);
|
|
$this->setzeZustand('Tageslaufzeit', 0);
|
|
$this->setzeZustand('SollwertGueltig', false);
|
|
$this->setzeZustand('Rueckmeldefehler', false);
|
|
$this->WriteAttributeBoolean('Initialisiert', true);
|
|
}
|
|
|
|
if ($this->ReadAttributeInteger('BestaetigtSeit') <= 0) {
|
|
// Migration bestehender Instanzen ohne bestaetigten Zeitbezug.
|
|
$this->WriteAttributeBoolean('Schaltziel', $schaltzustand);
|
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
|
$this->bestaetigeZustandswechsel($schaltzustand, $jetzt);
|
|
} else {
|
|
$this->setzeSchaltzustand($schaltzustand);
|
|
$this->setzeZustand(
|
|
'SchaltbefehlAusstehend',
|
|
$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
|
);
|
|
}
|
|
}
|
|
|
|
private function setzeSchaltzustand(bool $eingeschaltet): void
|
|
{
|
|
$this->setzeZustand('Schaltzustand', $eingeschaltet);
|
|
$this->setzeZustand('Istleistung', (float) $this->leistungFuerSchaltzustand($eingeschaltet));
|
|
$this->setzeZustand('Leistungsquelle', Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET);
|
|
}
|
|
|
|
private function leseSchaltzustand(): bool
|
|
{
|
|
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
|
if ($rueckmeldungID > 0) {
|
|
return (bool) GetValue($rueckmeldungID);
|
|
}
|
|
|
|
return $this->leseAktorSchaltzustand();
|
|
}
|
|
|
|
private function leseAktorSchaltzustand(): bool
|
|
{
|
|
$kontaktwert = (bool) GetValue($this->ReadPropertyInteger('SchaltkontaktVariableID'));
|
|
|
|
return $kontaktwert !== $this->ReadPropertyBoolean('SchaltkontaktInvertiert');
|
|
}
|
|
|
|
private function leistungFuerSchaltzustand(bool $eingeschaltet): int
|
|
{
|
|
return $eingeschaltet ? $this->ReadPropertyInteger('Nennleistung') : 0;
|
|
}
|
|
|
|
private function aktualisiereStoerung(): void
|
|
{
|
|
$texte = [];
|
|
if ($this->ReadAttributeString('Schaltfehler') !== '') {
|
|
$texte[] = $this->ReadAttributeString('Schaltfehler');
|
|
}
|
|
if ((bool) $this->leseZustand('Rueckmeldefehler')) {
|
|
$texte[] = 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.';
|
|
}
|
|
$this->setzeStoerung(implode("\n", $texte), $texte === []);
|
|
}
|
|
|
|
private function setzeStoerung(string $text, bool $fehlerfrei): void
|
|
{
|
|
$this->setzeZustand('Stoerung', !$fehlerfrei);
|
|
$this->setzeZustand('Stoertext', $text);
|
|
}
|
|
|
|
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
|
|
{
|
|
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->leseZustand('AenderungMoeglich'),
|
|
'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'),
|
|
'Istleistung_W' => (float) $this->leseZustand('Istleistung'),
|
|
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET,
|
|
'Zustand' => [
|
|
[
|
|
'Kennung' => 'Sollleistung_W',
|
|
'Art' => 'Sollwert',
|
|
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
|
|
? (int) $this->leseZustand('Sollleistung')
|
|
: null,
|
|
'Einheit' => 'W',
|
|
],
|
|
[
|
|
'Kennung' => 'Schaltzustand',
|
|
'Art' => 'Status',
|
|
'Wert' => (bool) $this->leseZustand('Schaltzustand'),
|
|
'Einheit' => '',
|
|
],
|
|
[
|
|
'Kennung' => 'SchaltbefehlAusstehend',
|
|
'Art' => 'Status',
|
|
'Wert' => (bool) $this->leseZustand('SchaltbefehlAusstehend'),
|
|
'Einheit' => '',
|
|
],
|
|
[
|
|
'Kennung' => 'Schaltbereit',
|
|
'Art' => 'Status',
|
|
'Wert' => (bool) $this->leseZustand('AenderungMoeglich'),
|
|
'Einheit' => '',
|
|
],
|
|
[
|
|
'Kennung' => 'RestMindestzeit_s',
|
|
'Art' => 'Istwert',
|
|
'Wert' => (int) $this->leseZustand('RestMindestzeit'),
|
|
'Einheit' => 's',
|
|
],
|
|
[
|
|
'Kennung' => 'Tageslaufzeit_s',
|
|
'Art' => 'Istwert',
|
|
'Wert' => (int) $this->leseZustand('Tageslaufzeit'),
|
|
'Einheit' => 's',
|
|
],
|
|
[
|
|
'Kennung' => 'Rueckmeldefehler',
|
|
'Art' => 'Stoerung',
|
|
'Wert' => (bool) $this->leseZustand('Rueckmeldefehler'),
|
|
'Einheit' => '',
|
|
'Text' => (bool) $this->leseZustand('Rueckmeldefehler')
|
|
? 'Schaltzustands-Rueckmeldung entspricht nicht dem angeforderten Zustand.'
|
|
: '',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @return list<int> */
|
|
private function zugeordneteManagerIDs(): array
|
|
{
|
|
$ergebnis = [];
|
|
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
|
|
try {
|
|
$automatisch = (bool) IPS_GetProperty($managerID, 'AutomatischeSuche');
|
|
$property = $automatisch
|
|
? 'AutomatischeVerbraucherZuordnung'
|
|
: 'VerbraucherZuordnung';
|
|
$zuordnung = $this->leseManagerZuordnung($managerID, $property);
|
|
if ($automatisch && $zuordnung === []) {
|
|
$zuordnung = $this->leseManagerZuordnung($managerID, 'VerbraucherZuordnung');
|
|
}
|
|
} catch (Throwable $fehler) {
|
|
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));
|
|
}
|
|
|
|
/** @return array<mixed> */
|
|
private function leseManagerZuordnung(int $managerID, string $property): array
|
|
{
|
|
$zuordnung = json_decode(
|
|
(string) IPS_GetProperty($managerID, $property),
|
|
true,
|
|
512,
|
|
JSON_THROW_ON_ERROR
|
|
);
|
|
|
|
return is_array($zuordnung) ? $zuordnung : [];
|
|
}
|
|
|
|
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'] as $property) {
|
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
|
}
|
|
}
|
|
if ($this->ReadPropertyInteger('Nennleistung') <= 0) {
|
|
throw new InvalidArgumentException('Nennleistung muss groesser als 0 sein.');
|
|
}
|
|
if ($this->ReadPropertyInteger('Mindestlaufzeit') < 0
|
|
|| $this->ReadPropertyInteger('Mindestlaufzeit') > 86400
|
|
) {
|
|
throw new InvalidArgumentException('Mindestlaufzeit muss zwischen 0 und 86400 Sekunden liegen.');
|
|
}
|
|
foreach (['Mindesteinschaltdauer', 'Mindestausschaltdauer'] as $property) {
|
|
if ($this->ReadPropertyInteger($property) < 0) {
|
|
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
|
|
}
|
|
}
|
|
|
|
$kontaktID = $this->ReadPropertyInteger('SchaltkontaktVariableID');
|
|
$this->pruefeBooleanVariable($kontaktID, 'Schaltkontakt', true);
|
|
|
|
$rueckmeldungID = $this->ReadPropertyInteger('RueckmeldungVariableID');
|
|
if ($rueckmeldungID > 0) {
|
|
$this->pruefeBooleanVariable($rueckmeldungID, 'Rueckmeldung', false);
|
|
}
|
|
}
|
|
|
|
private function pruefeBooleanVariable(int $variableID, string $bezeichnung, bool $aktionErforderlich): void
|
|
{
|
|
if ($variableID <= 0 || !IPS_VariableExists($variableID)) {
|
|
throw new InvalidArgumentException($bezeichnung . ' ist nicht eingerichtet.');
|
|
}
|
|
$variable = IPS_GetVariable($variableID);
|
|
if ((int) $variable['VariableType'] !== 0) {
|
|
throw new InvalidArgumentException($bezeichnung . ' muss eine Booleanvariable sein.');
|
|
}
|
|
if ($aktionErforderlich
|
|
&& (int) $variable['VariableAction'] <= 0
|
|
&& (int) $variable['VariableCustomAction'] <= 0
|
|
) {
|
|
throw new InvalidArgumentException($bezeichnung . ' benoetigt eine Standard- oder benutzerdefinierte Aktion.');
|
|
}
|
|
}
|
|
|
|
private function registriereMeldungen(): void
|
|
{
|
|
$alteIDs = array_unique(array_filter([
|
|
$this->ReadAttributeInteger('RegistrierterSchaltkontakt'),
|
|
$this->ReadAttributeInteger('RegistrierteRueckmeldung'),
|
|
]));
|
|
$neueIDs = array_unique(array_filter([
|
|
$this->ReadPropertyInteger('SchaltkontaktVariableID'),
|
|
$this->ReadPropertyInteger('RueckmeldungVariableID'),
|
|
]));
|
|
|
|
foreach ($alteIDs as $alteID) {
|
|
if (!in_array($alteID, $neueIDs, true)) {
|
|
$this->UnregisterMessage($alteID, self::VM_UPDATE);
|
|
}
|
|
}
|
|
foreach ($neueIDs as $neueID) {
|
|
if (IPS_VariableExists($neueID)) {
|
|
$this->RegisterMessage($neueID, self::VM_UPDATE);
|
|
}
|
|
}
|
|
|
|
$this->WriteAttributeInteger(
|
|
'RegistrierterSchaltkontakt',
|
|
$this->ReadPropertyInteger('SchaltkontaktVariableID')
|
|
);
|
|
$this->WriteAttributeInteger(
|
|
'RegistrierteRueckmeldung',
|
|
$this->ReadPropertyInteger('RueckmeldungVariableID')
|
|
);
|
|
}
|
|
|
|
private function aktualisiereVariablen(): void
|
|
{
|
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
|
if ($ident === 'Leistungsquelle') {
|
|
continue;
|
|
}
|
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
|
$this->setzeZustand($ident, GetValue($variablenID));
|
|
}
|
|
}
|
|
|
|
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
|
$this->registriereVerbraucherDiagnose();
|
|
$this->RegisterVariableBoolean('Rueckmeldefehler', 'Rueckmeldefehler', '~Alert', 200);
|
|
$this->RegisterVariableBoolean(
|
|
'SchaltbefehlAusstehend',
|
|
'Schaltbefehl ausstehend',
|
|
'~Switch',
|
|
210
|
|
);
|
|
$this->RegisterVariableInteger('RestMindestzeit', 'Restliche Mindestzeit', '', 220);
|
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
|
$wert = $ident === 'Leistungsquelle'
|
|
? Nachrichtenvertrag::LEISTUNGSQUELLE_BERECHNET
|
|
: $this->leseZustand($ident);
|
|
$this->setzeZustand($ident, $wert);
|
|
}
|
|
} else {
|
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
|
$this->UnregisterVariable($ident);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param bool|int|float|string $wert */
|
|
private function setzeZustand(string $ident, $wert): void
|
|
{
|
|
$booleanAttribute = [
|
|
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
|
'Verfuegbar' => 'ZustandVerfuegbar',
|
|
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
|
'Stoerung' => 'ZustandStoerung',
|
|
'Schaltzustand' => 'ZustandSchaltzustand',
|
|
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
|
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
|
|
];
|
|
$integerAttribute = [
|
|
'Sollleistung' => 'ZustandSollleistung',
|
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
|
'RestMindestzeit' => 'ZustandRestMindestzeit',
|
|
];
|
|
$floatAttribute = [
|
|
'Istleistung' => 'ZustandIstleistung',
|
|
];
|
|
$stringAttribute = [
|
|
'Stoertext' => 'ZustandStoertext',
|
|
];
|
|
|
|
if (isset($booleanAttribute[$ident])) {
|
|
$wert = (bool) $wert;
|
|
$this->WriteAttributeBoolean($booleanAttribute[$ident], $wert);
|
|
} elseif (isset($integerAttribute[$ident])) {
|
|
$wert = (int) $wert;
|
|
$this->WriteAttributeInteger($integerAttribute[$ident], $wert);
|
|
} elseif (isset($floatAttribute[$ident])) {
|
|
$wert = (float) $wert;
|
|
$this->WriteAttributeFloat($floatAttribute[$ident], $wert);
|
|
} elseif (isset($stringAttribute[$ident])) {
|
|
$wert = (string) $wert;
|
|
$this->WriteAttributeString($stringAttribute[$ident], $wert);
|
|
} elseif ($ident !== 'Leistungsquelle') {
|
|
throw new LogicException('Unbekannter interner Zustand: ' . $ident);
|
|
}
|
|
|
|
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
|
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
|
|
SetValue($variablenID, $wert);
|
|
}
|
|
}
|
|
|
|
/** @return bool|int|float|string */
|
|
private function leseZustand(string $ident)
|
|
{
|
|
$booleanAttribute = [
|
|
'SollwertGueltig' => 'ZustandSollwertGueltig',
|
|
'Verfuegbar' => 'ZustandVerfuegbar',
|
|
'AenderungMoeglich' => 'ZustandAenderungMoeglich',
|
|
'Stoerung' => 'ZustandStoerung',
|
|
'Schaltzustand' => 'ZustandSchaltzustand',
|
|
'Rueckmeldefehler' => 'ZustandRueckmeldefehler',
|
|
'SchaltbefehlAusstehend' => 'ZustandSchaltbefehlAusstehend',
|
|
];
|
|
$integerAttribute = [
|
|
'Sollleistung' => 'ZustandSollleistung',
|
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
|
'RestMindestzeit' => 'ZustandRestMindestzeit',
|
|
];
|
|
$floatAttribute = [
|
|
'Istleistung' => 'ZustandIstleistung',
|
|
];
|
|
$stringAttribute = [
|
|
'Stoertext' => 'ZustandStoertext',
|
|
];
|
|
|
|
if (isset($booleanAttribute[$ident])) {
|
|
return $this->ReadAttributeBoolean($booleanAttribute[$ident]);
|
|
}
|
|
if (isset($integerAttribute[$ident])) {
|
|
return $this->ReadAttributeInteger($integerAttribute[$ident]);
|
|
}
|
|
if (isset($floatAttribute[$ident])) {
|
|
return $this->ReadAttributeFloat($floatAttribute[$ident]);
|
|
}
|
|
if (isset($stringAttribute[$ident])) {
|
|
return $this->ReadAttributeString($stringAttribute[$ident]);
|
|
}
|
|
|
|
throw new LogicException('Unbekannter interner Zustand: ' . $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 zeitzone(): DateTimeZone
|
|
{
|
|
return new DateTimeZone(date_default_timezone_get());
|
|
}
|
|
|
|
private function deaktiviereTimer(): void
|
|
{
|
|
foreach ([
|
|
'Meldezyklus',
|
|
'RueckmeldungVerzoegert',
|
|
'Umschaltfreigabe',
|
|
'MindestzeitAbgelaufen',
|
|
'VorgabeTimeout',
|
|
'Tagesplanung',
|
|
] as $timer) {
|
|
$this->SetTimerInterval($timer, 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
|
|
);
|
|
}
|
|
}
|