This commit is contained in:
@@ -19,6 +19,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
private const VERBRAUCHER_MODUL_IDS = [
|
private const VERBRAUCHER_MODUL_IDS = [
|
||||||
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}',
|
||||||
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
|
'{15879A4E-D0C2-4495-83DE-46E1E462591E}',
|
||||||
];
|
];
|
||||||
private const MONATSNAMEN = [
|
private const MONATSNAMEN = [
|
||||||
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
1 => 'Januar', 'Februar', 'Maerz', 'April', 'Mai', 'Juni',
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ Energiemanagement, Manager und steuerbare Verbraucher fuer IP-Symcon.
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
Das Repository befindet sich im Aufbau. Der gemeinsame Nachrichtenvertrag, die
|
||||||
Verbraucherbasis, der Manager und der mehrstufige Warmwassererwaermer sind
|
Verbraucherbasis, der Manager, der mehrstufige Warmwassererwaermer und der
|
||||||
implementiert. Die weiteren Module sind als Diskussionsentwürfe dokumentiert.
|
1-stufige Verbraucher sind implementiert. Die weiteren Module sind als
|
||||||
|
Diskussionsentwürfe dokumentiert.
|
||||||
|
|
||||||
## Geplante Module
|
## Geplante Module
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ implementiert. Die weiteren Module sind als Diskussionsentwürfe dokumentiert.
|
|||||||
- Batterie
|
- Batterie
|
||||||
- Wassererwaermer (implementiert)
|
- Wassererwaermer (implementiert)
|
||||||
- Pufferspeicher
|
- Pufferspeicher
|
||||||
- Verbraucher 1-Stufig
|
- Verbraucher 1-Stufig (implementiert)
|
||||||
- Waermepumpe
|
- Waermepumpe
|
||||||
- Ladestation Stand-Alone
|
- Ladestation Stand-Alone
|
||||||
- Ladestation Gateway
|
- Ladestation Gateway
|
||||||
@@ -51,6 +52,8 @@ Neue oder geaenderte Funktionen werden zusammen mit ihren PHPUnit-Tests eingeche
|
|||||||
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
- [Manager-Verbraucher-Schnittstelle](docs/Schnittstelle.md)
|
||||||
- [Manager-Modul](docs/module/Manager/README.md)
|
- [Manager-Modul](docs/module/Manager/README.md)
|
||||||
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
- [Warmwassererwaermer-Modul](docs/module/Wassererwaermer/README.md)
|
||||||
|
- [Verbraucher-1-Stufig-Modul](docs/module/Verbraucher-1-Stufig/README.md)
|
||||||
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
- [Migration Boiler x-Stufig](docs/migration/Boiler-x-Stufig.md)
|
||||||
|
- [Migration Verbraucher 1-Stufig](docs/migration/Verbraucher-1-Stufig.md)
|
||||||
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
- [Obere Anschlüsse des Managers](docs/Obere-Anschluesse.md)
|
||||||
- [Modulübersicht](docs/module/README.md)
|
- [Modulübersicht](docs/module/README.md)
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Manager und Zeitverhalten",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPV",
|
||||||
|
"caption": "Prioritaet PV",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "PrioritaetPeak",
|
||||||
|
"caption": "Prioritaet Peak",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Meldeintervall",
|
||||||
|
"caption": "Meldeintervall",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "VorgabeTimeout",
|
||||||
|
"caption": "Vorgabe-Timeout",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Umschaltabstand",
|
||||||
|
"caption": "Zeit bis zum naechsten Lastwechsel",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Verbraucher",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Nennleistung",
|
||||||
|
"caption": "Nennleistung",
|
||||||
|
"suffix": " W",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "SchaltkontaktVariableID",
|
||||||
|
"caption": "Schaltkontakt",
|
||||||
|
"validVariableTypes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "SchaltkontaktInvertiert",
|
||||||
|
"caption": "Schaltkontakt invertieren"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SelectVariable",
|
||||||
|
"name": "RueckmeldungVariableID",
|
||||||
|
"caption": "Optionale Schaltzustands-Rueckmeldung",
|
||||||
|
"validVariableTypes": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Mindestlaufzeit",
|
||||||
|
"caption": "Taegliche Mindestlaufzeit",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 86400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Darstellung und Diagnose",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "EinstellungenInVisu",
|
||||||
|
"caption": "Lokale Einstellungen in der Visualisierung anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "DiagnosevariablenAnzeigen",
|
||||||
|
"caption": "Diagnosevariablen anzeigen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "LoggingEin",
|
||||||
|
"caption": "Diagnoseprotokoll aktivieren"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "{15879A4E-D0C2-4495-83DE-46E1E462591E}",
|
||||||
|
"name": "VerbraucherEinStufig",
|
||||||
|
"type": 3,
|
||||||
|
"vendor": "Enelix",
|
||||||
|
"aliases": [
|
||||||
|
"Verbraucher 1-Stufig"
|
||||||
|
],
|
||||||
|
"parentRequirements": [],
|
||||||
|
"childRequirements": [],
|
||||||
|
"implemented": [],
|
||||||
|
"prefix": "ENELIX",
|
||||||
|
"url": "https://git.belevo.ch/ENELIX/Enelix-EMS/src/branch/develop/VerbraucherEinStufig"
|
||||||
|
}
|
||||||
@@ -0,0 +1,861 @@
|
|||||||
|
<?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',
|
||||||
|
];
|
||||||
|
|
||||||
|
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('Umschaltabstand', 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('GesperrtBis', 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->RegisterTimer(
|
||||||
|
'Meldezyklus',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'RueckmeldungVerzoegert',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
|
||||||
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'Umschaltfreigabe',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'Umschaltfreigabe', 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 'Umschaltfreigabe':
|
||||||
|
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->setzeSchaltzustand($schaltzustand);
|
||||||
|
if ($this->ReadAttributeBoolean('Initialisiert')
|
||||||
|
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend')
|
||||||
|
) {
|
||||||
|
$this->starteUmschaltsperre($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');
|
||||||
|
$fehlerfrei = $this->ReadAttributeString('Schaltfehler') === ''
|
||||||
|
&& !(bool) $this->leseZustand('Rueckmeldefehler');
|
||||||
|
$verfuegbar = $aktiv && $fehlerfrei;
|
||||||
|
$mindestlaufzeitErzwungen = $verfuegbar
|
||||||
|
&& $this->mussMindestlaufzeitErzwingen($jetzt);
|
||||||
|
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
|
||||||
|
$ziel = 0;
|
||||||
|
if ($verfuegbar) {
|
||||||
|
if ($mindestlaufzeitErzwungen) {
|
||||||
|
$ziel = $this->ReadPropertyInteger('Nennleistung');
|
||||||
|
} elseif ((bool) $this->leseZustand('SollwertGueltig')) {
|
||||||
|
$ziel = (int) $this->leseZustand('Sollleistung');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||||
|
if ($ziel !== $istleistung && (!$aktiv || (!$gesperrt && $fehlerfrei))) {
|
||||||
|
$this->schalte($ziel > 0, $jetzt);
|
||||||
|
$schaltzustand = $this->leseSchaltzustand();
|
||||||
|
$this->setzeSchaltzustand($schaltzustand);
|
||||||
|
$istleistung = $this->leistungFuerSchaltzustand($schaltzustand);
|
||||||
|
$gesperrt = $jetzt < $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setzeZustand('Sollleistung', $ziel);
|
||||||
|
$this->setzeZustand('Verfuegbar', $verfuegbar);
|
||||||
|
$aenderungMoeglich = $verfuegbar
|
||||||
|
&& !$mindestlaufzeitErzwungen
|
||||||
|
&& !$gesperrt
|
||||||
|
&& !$this->ReadAttributeBoolean('SchaltpruefungAusstehend');
|
||||||
|
$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->WriteAttributeBoolean('SchaltvorgangLaeuft', true);
|
||||||
|
try {
|
||||||
|
RequestAction($kontaktID, $aktorwert);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
throw new RuntimeException('Schaltkontakt konnte nicht gesetzt werden: ' . $fehler->getMessage());
|
||||||
|
} finally {
|
||||||
|
$this->WriteAttributeBoolean('SchaltvorgangLaeuft', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->starteUmschaltsperre($jetzt);
|
||||||
|
$istzustand = $this->leseSchaltzustand();
|
||||||
|
if ($istzustand === $einschalten) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
} elseif ($this->ReadPropertyInteger('RueckmeldungVariableID') <= 0) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
throw new RuntimeException('Schaltkontakt hat den angeforderten Zustand nicht uebernommen.');
|
||||||
|
} elseif ($this->ReadPropertyInteger('Umschaltabstand') === 0) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeString('Schaltfehler', '');
|
||||||
|
$this->protokolliere('Lastwechsel', ['Eingeschaltet' => $einschalten]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pruefeAusstehendenSchaltvorgang(bool $schaltzustand, int $jetzt): void
|
||||||
|
{
|
||||||
|
if (!$this->ReadAttributeBoolean('SchaltpruefungAusstehend')) {
|
||||||
|
if ((bool) $this->leseZustand('Rueckmeldefehler')
|
||||||
|
&& $schaltzustand === $this->ReadAttributeBoolean('Schaltziel')
|
||||||
|
) {
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($schaltzustand === $this->ReadAttributeBoolean('Schaltziel')) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($jetzt >= $this->ReadAttributeInteger('GesperrtBis')) {
|
||||||
|
$this->WriteAttributeBoolean('SchaltpruefungAusstehend', false);
|
||||||
|
$this->setzeZustand('Rueckmeldefehler', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function starteUmschaltsperre(int $jetzt): void
|
||||||
|
{
|
||||||
|
$gesperrtBis = $jetzt + $this->ReadPropertyInteger('Umschaltabstand');
|
||||||
|
$this->WriteAttributeInteger('GesperrtBis', $gesperrtBis);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$gesperrtBis = $this->ReadAttributeInteger('GesperrtBis');
|
||||||
|
$this->SetTimerInterval(
|
||||||
|
'Umschaltfreigabe',
|
||||||
|
$gesperrtBis > $jetzt ? max(1, ($gesperrtBis - $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);
|
||||||
|
}
|
||||||
|
$this->setzeSchaltzustand($schaltzustand);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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' => '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.');
|
||||||
|
}
|
||||||
|
if ($this->ReadPropertyInteger('Umschaltabstand') < 0) {
|
||||||
|
throw new InvalidArgumentException('Umschaltabstand 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);
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||||
|
];
|
||||||
|
$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',
|
||||||
|
];
|
||||||
|
$integerAttribute = [
|
||||||
|
'Sollleistung' => 'ZustandSollleistung',
|
||||||
|
'Tageslaufzeit' => 'ZustandTageslaufzeit',
|
||||||
|
];
|
||||||
|
$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',
|
||||||
|
'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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ADR 0002: Verbraucher 1-Stufig arbeitet ereignisbasiert
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Akzeptiert.
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Das Enelix-1-Modul berechnete seinen Zustand in einem festen Intervall und
|
||||||
|
bildete die Lastwechselsperre ueber `Interval`, `IdleCounterMax` und weitere
|
||||||
|
zyklusabhaengige Zaehler ab. Dieses Verhalten passt nicht zur
|
||||||
|
ereignisbasierten Enelix-2-Kommunikation und machte reale Zeitabstaende von
|
||||||
|
mehreren Properties abhaengig.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Der neue `VerbraucherEinStufig` verwendet keine zyklische Regelberechnung.
|
||||||
|
Schaltkontakt, Rueckmeldung, Manager-Vorgabe, Freigabe, Vorgabeablauf und
|
||||||
|
Tagesplanung loesen die Regelung direkt aus.
|
||||||
|
|
||||||
|
Als einziger modulspezifischer Wert fuer die Lastwechselbegrenzung wird
|
||||||
|
`Umschaltabstand` in Sekunden registriert. Der Standardwert ist 5 Sekunden.
|
||||||
|
Das Ende der Sperre wird durch einen einmaligen Timer ausgeloest.
|
||||||
|
|
||||||
|
Das gemeinsame `Meldeintervall` bleibt erhalten, weil Vertrag `3.0`
|
||||||
|
zusaetzlich zu Ereignismeldungen eine periodische Vollmeldung fordert. Dieser
|
||||||
|
Timer ist kein Regelzyklus.
|
||||||
|
|
||||||
|
Die Tagesmindestlaufzeit wird in realen Sekunden und lokaler Symcon-Zeitzone
|
||||||
|
gezaehlt. Das Modul erzwingt den Betrieb erst zum spaetestmoeglichen Zeitpunkt,
|
||||||
|
an dem die fehlende Laufzeit vor Mitternacht noch erreicht werden kann.
|
||||||
|
|
||||||
|
## Alternativen
|
||||||
|
|
||||||
|
- Ein fester Regelzyklus wurde verworfen, weil Reaktionszeit und Sperrzeit
|
||||||
|
wieder voneinander abhaengen wuerden.
|
||||||
|
- Getrennte Ein- und Ausschaltzeiten wurden verworfen, weil sie fuer diese
|
||||||
|
Adaption nicht freigegeben sind.
|
||||||
|
- Die feste Nachtphase aus Enelix 1 wurde verworfen, weil sie weder
|
||||||
|
konfigurierbar noch Teil der Enelix-2-Spezifikation ist.
|
||||||
|
|
||||||
|
## Folgen
|
||||||
|
|
||||||
|
- Lastwechsel reagieren ohne Polling auf relevante Ereignisse.
|
||||||
|
- `Interval` und `IdleCounterMax` entfallen.
|
||||||
|
- Der Manager sieht waehrend einer Sperre nur die aktuelle Leistung und
|
||||||
|
`AenderungMoeglich=false`.
|
||||||
|
- Ein optionaler Rueckmeldekontakt muss innerhalb des Umschaltabstands folgen.
|
||||||
|
- Tageslaufzeit und Mindestlaufzeit sind unabhaengig von einer Zyklusdauer.
|
||||||
|
- Das Modul bleibt vollstaendig im Repository Enelix EMS; Enelix Utils erhaelt
|
||||||
|
keine geraetespezifische Logik.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
|
||||||
|
- Eine spaetere sperrbare Variante wird separat spezifiziert.
|
||||||
|
- Unterschiedliche Ein- und Ausschaltabstaende benoetigen eine neue
|
||||||
|
Architekturentscheidung.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Migration von Verbraucher_1_Stufig
|
||||||
|
|
||||||
|
Das Enelix-1-Modul wird nicht in-place aktualisiert. Das neue Modul verwendet
|
||||||
|
eine neue Modul-ID, den Vertrag `3.0` und eine ereignisbasierte Regelung.
|
||||||
|
|
||||||
|
## Eigenschaften
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 | Migration |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `BoilerLeistung` | `Nennleistung` | Wert in ganzen Watt uebernehmen. |
|
||||||
|
| `Schaltkontakt1` | `SchaltkontaktVariableID` | Boolean-Aktor uebernehmen und Aktion pruefen. |
|
||||||
|
| nicht vorhanden | `SchaltkontaktInvertiert` | Nur bei umgekehrter Aktorlogik aktivieren. |
|
||||||
|
| nicht vorhanden | `RueckmeldungVariableID` | Optional eine echte Boolean-Rueckmeldung zuordnen. |
|
||||||
|
| `Mindesttlaufzeit` | `Mindestlaufzeit` | Alter Wert war in Minuten gedacht; fuer gleiches Verhalten mit 60 multiplizieren und als Sekunden eintragen. |
|
||||||
|
| `Zeit_Zwischen_Zustandswechseln` | `Umschaltabstand` | Alte Minuten nicht uebernehmen. Neuen Sekundenwert bewusst festlegen; Standard ist 5 s. |
|
||||||
|
| `Interval` | entfaellt | Keine zyklische Regelberechnung mehr. |
|
||||||
|
| `IdleCounterMax` | entfaellt | Keine zyklusbasierte Idle-Erkennung mehr. |
|
||||||
|
|
||||||
|
## Variablen und Verhalten
|
||||||
|
|
||||||
|
| Enelix 1 | Enelix 2 |
|
||||||
|
| --- | --- |
|
||||||
|
| `DailyOnTime` | `Tageslaufzeit` in echten Sekunden |
|
||||||
|
| `IstNacht` | entfaellt; Mindestlaufzeit wird am spaetestmoeglichen Start des lokalen Kalendertags erzwungen |
|
||||||
|
| `IsTimerActive` | entfaellt; interne einmalige Timer |
|
||||||
|
| `Aktuelle_Leistung` | gemeinsames `Istleistung` |
|
||||||
|
| `Power` | gemeinsames `Sollleistung` |
|
||||||
|
| `PowerSteps` | Vertragsfeld `Leistungswerte_W` |
|
||||||
|
| `PV_Prio` | Property `PrioritaetPV` |
|
||||||
|
| `Sperre_Prio` | Property `PrioritaetPeak` |
|
||||||
|
| `Is_Peak_Shaving` | entfaellt; Betriebsart liegt im Manager |
|
||||||
|
| `Leistung_Delta` | entfaellt; Leistungsverteilung liegt im Manager |
|
||||||
|
| `Idle`, `IdleCounter` | entfallen |
|
||||||
|
|
||||||
|
Die feste Enelix-1-Nachtzeit von 22:00 bis 07:00 wird nicht uebernommen.
|
||||||
|
Enelix 2 plant nur den Zeitpunkt, ab dem die verbleibende Mindestlaufzeit vor
|
||||||
|
dem lokalen Tagesende zwingend eingeschaltet werden muss.
|
||||||
|
|
||||||
|
## Inbetriebnahme
|
||||||
|
|
||||||
|
1. Bestehende Instanzkonfiguration und aktuelle Aktorlogik dokumentieren.
|
||||||
|
2. Neue Instanz **Verbraucher 1-Stufig** anlegen.
|
||||||
|
3. Leistung, Aktor, Invertierung und optional Rueckmeldung konfigurieren.
|
||||||
|
4. Mindestlaufzeit von Minuten in Sekunden umrechnen.
|
||||||
|
5. `Umschaltabstand` neu in Sekunden festlegen; nicht den alten Minutenwert
|
||||||
|
kopieren.
|
||||||
|
6. Neue Instanz im Enelix-2-Manager aktiv zuordnen.
|
||||||
|
7. Bei ausgeschalteter lokaler EMS-Freigabe Ein/Aus und Rueckmeldung
|
||||||
|
beaufsichtigt pruefen.
|
||||||
|
8. Alte Instanz erst deaktivieren, danach `Aktiv` an der neuen Instanz
|
||||||
|
einschalten.
|
||||||
|
9. Mindestens einen Timeout, einen verhinderten Lastwechsel und einen
|
||||||
|
Tageswechsel kontrollieren.
|
||||||
|
|
||||||
|
## Rueckkehr
|
||||||
|
|
||||||
|
Solange die alte Instanz nicht geloescht wurde, kann zurueckgekehrt werden,
|
||||||
|
indem die neue Instanz im Manager deaktiviert und ausgeschaltet wird. Danach
|
||||||
|
darf die alte Instanz wieder aktiviert werden. Beide Module duerfen nie
|
||||||
|
gleichzeitig denselben Aktor steuern.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# EMS-Module und Modulentwürfe
|
# EMS-Module und Modulentwürfe
|
||||||
|
|
||||||
> Manager und Warmwassererwaermer sind als installierbare IP-Symcon-Module
|
> Manager, Warmwassererwaermer und Verbraucher 1-Stufig sind als installierbare
|
||||||
> umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
> IP-Symcon-Module umgesetzt. Die weiteren Ordner enthalten Besprechungsgrundlagen.
|
||||||
|
|
||||||
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
Alle steuerbaren Verbraucher verwenden die gemeinsamen Datenpunkte aus der
|
||||||
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
[EMS-Schnittstelle](../Schnittstelle.md). In den Modul-READMEs stehen deshalb
|
||||||
@@ -13,7 +13,7 @@ nur zusätzliche Properties, Variablen, Zustände und offene Punkte.
|
|||||||
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers |
|
| [Batterie](Batterie/README.md) | Laden und Entladen eines Speichers |
|
||||||
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
| [Wassererwärmer](Wassererwaermer/README.md) | Mehrstufige elektrische Erwärmung (implementiert) |
|
||||||
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
| [Pufferspeicher](Pufferspeicher/README.md) | Heizstufen nach Heizkurve |
|
||||||
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher |
|
| [Verbraucher 1-Stufig](Verbraucher-1-Stufig/README.md) | Ein-/Aus-Verbraucher (implementiert) |
|
||||||
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
| [Wärmepumpe](Waermepumpe/README.md) | Sperrkontakt oder SG Ready |
|
||||||
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung |
|
| [Ladestation Stand-Alone](Ladestation-Stand-Alone/README.md) | Direkte Geräteanbindung |
|
||||||
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Ladestation am Easee Gateway |
|
| [Ladestation Gateway](Ladestation-Gateway/README.md) | Ladestation am Easee Gateway |
|
||||||
|
|||||||
@@ -1,36 +1,156 @@
|
|||||||
# Verbraucher 1-Stufig
|
# Verbraucher 1-Stufig
|
||||||
|
|
||||||
> Status: Diskussionsentwurf.
|
> Status: implementiert fuer IP-Symcon 8 und den Enelix-2-Vertrag `3.0`.
|
||||||
|
|
||||||
Schaltet eine elektrische Last. Das Leistungsangebot ist üblicherweise
|
Das Modul schaltet einen elektrischen Ein-/Aus-Verbraucher. Im freien Zustand
|
||||||
`[0, Nennleistung]`.
|
meldet es dem Manager das Leistungsangebot `[0, Nennleistung]`. Waerend der
|
||||||
|
Umschaltsperre und bei lokal erzwungener Mindestlaufzeit wird nur der aktuell
|
||||||
|
zulaessige Leistungswert angeboten.
|
||||||
|
|
||||||
## Zusätzliche Variablen
|
## Architekturentscheidung
|
||||||
|
|
||||||
|
Die Regelung ist ereignisbasiert. Neuberechnungen werden ausgeloest durch:
|
||||||
|
|
||||||
|
- eine neue Manager-Vorgabe,
|
||||||
|
- eine Aenderung an Schaltkontakt oder optionaler Rueckmeldung,
|
||||||
|
- das Ein- oder Ausschalten der lokalen EMS-Freigabe,
|
||||||
|
- das Ende des Umschaltabstands,
|
||||||
|
- das Ablaufen einer Manager-Vorgabe,
|
||||||
|
- den naechsten relevanten Zeitpunkt der Tageslaufzeitplanung.
|
||||||
|
|
||||||
|
Es gibt keinen Regelzyklus, kein `Interval` und keinen `IdleCounterMax`.
|
||||||
|
`Meldeintervall` bleibt als vertraglich geforderte periodische Vollmeldung
|
||||||
|
bestehen und steuert keine Regelberechnung.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
Zu den sechs gemeinsamen Verbraucher-Properties aus
|
||||||
|
[`Schnittstelle.md`](../../Schnittstelle.md) kommen sieben
|
||||||
|
Modul-Properties hinzu.
|
||||||
|
|
||||||
|
| Ident | Typ | Standard | Beschreibung |
|
||||||
|
| --- | --- | ---: | --- |
|
||||||
|
| `Nennleistung` | Integer | `0` | Positive elektrische Leistung in W. |
|
||||||
|
| `SchaltkontaktVariableID` | Integer | `0` | Boolean-Aktor mit Standard- oder benutzerdefinierter Aktion. |
|
||||||
|
| `SchaltkontaktInvertiert` | Boolean | `false` | Kehrt die Ein-/Aus-Semantik des Aktors um. |
|
||||||
|
| `RueckmeldungVariableID` | Integer | `0` | Optionale Boolean-Rueckmeldung; `true` bedeutet eingeschaltet. |
|
||||||
|
| `Mindestlaufzeit` | Integer | `0` | Geforderte Laufzeit pro lokalem Kalendertag in Sekunden, maximal 86400. |
|
||||||
|
| `Umschaltabstand` | Integer | `5` | Sekunden bis zum naechsten erlaubten Lastwechsel. |
|
||||||
|
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Legt gemeinsame Diagnosevariablen und `Rueckmeldefehler` an. |
|
||||||
|
|
||||||
|
`EinstellungenInVisu` ist Teil der gemeinsamen Verbraucherbasis. Das Modul
|
||||||
|
besitzt derzeit keine zusaetzlichen bedienbaren Einstellvariablen und wertet
|
||||||
|
diese Property deshalb noch nicht weiter aus.
|
||||||
|
|
||||||
|
## Variablen
|
||||||
|
|
||||||
|
Immer sichtbar:
|
||||||
|
|
||||||
| Ident | Typ / Zugriff | Beschreibung |
|
| Ident | Typ / Zugriff | Beschreibung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Schaltzustand` | Boolean / Anzeige | Rückgemeldeter oder berechneter Kontaktzustand. |
|
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; Startwert `false`. |
|
||||||
| `Tageslaufzeit` | Integer / Anzeige | Laufzeit seit lokalem Tagesbeginn in Sekunden; persistent mit Tageskennung. |
|
| `Schaltzustand` | Boolean / Anzeige | Rueckgemeldeter oder aus dem Aktor berechneter Zustand. |
|
||||||
|
| `Tageslaufzeit` | Integer / Anzeige | Laufzeit des lokalen Kalendertags in Sekunden. |
|
||||||
|
|
||||||
## Zusätzliche Properties
|
Mit `DiagnosevariablenAnzeigen` werden diese Variablen zusaetzlich angelegt:
|
||||||
|
|
||||||
| Ident | Typ | Standard / Beschreibung |
|
- `Istleistung`
|
||||||
| --- | --- | --- |
|
- `Leistungsquelle`
|
||||||
| `Nennleistung` | Float | `0*` W; grösser 0. |
|
- `Sollleistung`
|
||||||
| `SchaltkontaktVariableID` | Integer | `0`; erforderlicher Boolean-Aktor mit Action. |
|
- `SollwertGueltig`
|
||||||
| `SchaltkontaktInvertiert` | Boolean | `false`; Ein-/Aus-Semantik des Aktors. |
|
- `Verfuegbar`
|
||||||
| `RueckmeldungVariableID` | Integer | `0`; optionale echte Rückmeldung. |
|
- `AenderungMoeglich`
|
||||||
| `Mindestlaufzeit` | Integer | `0` s. |
|
- `Stoerung`
|
||||||
| `Umschaltabstand` | Integer | `60` s; Mindestabstand zwischen Änderungen. |
|
- `Stoertext`
|
||||||
|
- `Rueckmeldefehler`
|
||||||
|
|
||||||
Der frühere Ident `BoilerLeistung` wird durch `Nennleistung` ersetzt.
|
Die Regellogik arbeitet mit persistenten Attributen und ist nicht von der
|
||||||
|
Sichtbarkeit der Diagnosevariablen abhaengig.
|
||||||
|
|
||||||
## Zustand
|
## Zeit- und Schaltverhalten
|
||||||
|
|
||||||
`Schaltzustand` als Status, `Tageslaufzeit_s` als Istwert und
|
Nach jedem Lastwechsel wird `AenderungMoeglich=false` gemeldet und das
|
||||||
`Rueckmeldefehler` als Störung.
|
Leistungsangebot auf den aktuellen Wert eingeschraenkt. Nach
|
||||||
|
`Umschaltabstand` Sekunden gibt ein einmaliger Timer den naechsten
|
||||||
|
Lastwechsel frei. Der Standardwert betraegt 5 Sekunden.
|
||||||
|
|
||||||
## Offene Punkte
|
Die Tageslaufzeit wird mit der in IP-Symcon eingestellten lokalen Zeitzone
|
||||||
|
gefuehrt. Sommer- und Winterzeit sowie der Wechsel um Mitternacht werden
|
||||||
|
beruecksichtigt. Ist die konfigurierte Mindestlaufzeit noch nicht erreicht,
|
||||||
|
wird der Verbraucher erst zum spaetestmoeglichen Zeitpunkt lokal
|
||||||
|
eingeschaltet, an dem die Restlaufzeit bis Mitternacht noch erfuellt werden
|
||||||
|
kann. In dieser Phase meldet er `AenderungMoeglich=false`.
|
||||||
|
|
||||||
- Sperrbare Variante später integrieren und vollständig überarbeiten.
|
Bei Aktivierung sehr kurz vor Tagesende kann eine unmoeglich hohe Restlaufzeit
|
||||||
- Unterschiedliche Ein- und Ausschaltzeiten erst nach Freigabe ergänzen.
|
naturgemaess nicht mehr vollstaendig nachgeholt werden. Der Verbraucher wird
|
||||||
|
dann sofort eingeschaltet und die tatsaechliche Laufzeit erfasst.
|
||||||
|
|
||||||
|
## Rueckmeldung und Fehler
|
||||||
|
|
||||||
|
Ohne separate Rueckmeldung wird der Schaltzustand aus der Aktorvariable
|
||||||
|
berechnet. Mit `RueckmeldungVariableID` wartet das Modul bis zum Ende des
|
||||||
|
Umschaltabstands auf den angeforderten Zustand. Liegt er dann nicht an, werden
|
||||||
|
`Rueckmeldefehler=true`, `Stoerung=true` und `Verfuegbar=false` gemeldet.
|
||||||
|
|
||||||
|
Die Istleistung ist immer berechnet: `0` oder `Nennleistung`. Deshalb wird
|
||||||
|
`Leistungsquelle=1` gemeldet, auch wenn der Boolean-Schaltzustand ueber eine
|
||||||
|
physische Rueckmeldung erfasst wird.
|
||||||
|
|
||||||
|
## Managerkommunikation
|
||||||
|
|
||||||
|
Das Modul implementiert
|
||||||
|
`VerbraucherSchnittstelle::ManagerdatenEmpfangen()` und verwendet
|
||||||
|
ausschliesslich den Vertrag `3.0`. Der Verbraucher besitzt keine
|
||||||
|
Manager-ID-Property. Er akzeptiert nur Manager, in deren manueller oder
|
||||||
|
automatischer Verbraucherzuordnung seine Instanz aktiv eingetragen ist.
|
||||||
|
|
||||||
|
Rueckmeldungen erfolgen bei relevanten Ereignissen, kurz verzoegert nach einer
|
||||||
|
Manager-Vorgabe und zusaetzlich alle `Meldeintervall` Sekunden. Eine Vorgabe
|
||||||
|
wird nach `VorgabeTimeout` Sekunden ohne Erneuerung ungueltig.
|
||||||
|
|
||||||
|
## Installation und Inbetriebnahme
|
||||||
|
|
||||||
|
1. Im IP-Symcon Module Control den Testing-Branch `develop` der Bibliothek
|
||||||
|
`https://git.belevo.ch/ENELIX/Enelix-EMS.git` installieren oder
|
||||||
|
aktualisieren.
|
||||||
|
2. Unter **Instanz hinzufuegen** nach **Verbraucher 1-Stufig** suchen und eine
|
||||||
|
Instanz anlegen.
|
||||||
|
3. `Nennleistung` in ganzen Watt eintragen.
|
||||||
|
4. Als `Schaltkontakt` eine Booleanvariable mit funktionsfaehiger Aktion
|
||||||
|
auswaehlen. Bei umgekehrter Aktorlogik `SchaltkontaktInvertiert`
|
||||||
|
aktivieren.
|
||||||
|
5. Optional eine separate Boolean-Rueckmeldung auswaehlen. Dort muss
|
||||||
|
`true` dem physisch eingeschalteten Verbraucher entsprechen.
|
||||||
|
6. Prioritaeten, `Mindestlaufzeit`, `Umschaltabstand`,
|
||||||
|
`Meldeintervall` und `VorgabeTimeout` einstellen und die Konfiguration
|
||||||
|
uebernehmen.
|
||||||
|
7. Die neue Instanz im Manager manuell aktiv zuordnen oder bei automatischer
|
||||||
|
Suche in der gefundenen Liste aktivieren.
|
||||||
|
8. Fuer die Erstpruefung `DiagnosevariablenAnzeigen` und bei Bedarf
|
||||||
|
`LoggingEin` einschalten.
|
||||||
|
9. Unter Aufsicht `Aktiv` einschalten und ueber den Manager je eine Ein- und
|
||||||
|
Aus-Vorgabe pruefen.
|
||||||
|
|
||||||
|
## Abnahmecheckliste
|
||||||
|
|
||||||
|
- `Aktiv=false` schaltet den Ausgang aus.
|
||||||
|
- `Aktiv=true` und eine Manager-Vorgabe `Nennleistung` schalten den Ausgang
|
||||||
|
ein.
|
||||||
|
- Direkt nach einem Wechsel ist `AenderungMoeglich=false`; nach 5 Sekunden
|
||||||
|
wird es mit Standardkonfiguration wieder `true`.
|
||||||
|
- Eine Gegenanforderung waehrend der Sperre wird vom Leistungsangebot
|
||||||
|
ausgeschlossen.
|
||||||
|
- Die optionale Rueckmeldung folgt dem Ausgang innerhalb des
|
||||||
|
Umschaltabstands; andernfalls erscheint eine Stoerung.
|
||||||
|
- `Tageslaufzeit` steigt nur im eingeschalteten Zustand und beginnt am
|
||||||
|
lokalen Tageswechsel wieder bei 0.
|
||||||
|
- Nach Ablauf von `VorgabeTimeout` wird ohne neue Manager-Vorgabe
|
||||||
|
ausgeschaltet, sobald der Umschaltabstand dies zulaesst.
|
||||||
|
- Nach Neustart oder erneuter Konfiguration wird der reale Schaltzustand
|
||||||
|
eingelesen; alte Manager-Vorgaben werden nicht ungeprueft fortgesetzt.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
Die Zuordnung der Enelix-1-Felder, Einheiten und bewusst entfallenen
|
||||||
|
Kommunikationsvariablen steht in
|
||||||
|
[`docs/migration/Verbraucher-1-Stufig.md`](../../migration/Verbraucher-1-Stufig.md).
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class EinStufigRegler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<int>
|
||||||
|
*/
|
||||||
|
public static function leistungsangebot(
|
||||||
|
int $nennleistung,
|
||||||
|
int $istleistung,
|
||||||
|
bool $verfuegbar,
|
||||||
|
bool $aenderungMoeglich,
|
||||||
|
bool $mindestlaufzeitErzwungen
|
||||||
|
): array {
|
||||||
|
if ($nennleistung <= 0) {
|
||||||
|
throw new InvalidArgumentException('Die Nennleistung muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
if (!in_array($istleistung, [0, $nennleistung], true)) {
|
||||||
|
throw new InvalidArgumentException('Die Istleistung muss 0 oder der Nennleistung entsprechen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$verfuegbar || !$aenderungMoeglich || $mindestlaufzeitErzwungen) {
|
||||||
|
return [$istleistung];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0, $nennleistung];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mussMindestlaufzeitErzwingen(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende
|
||||||
|
): bool {
|
||||||
|
self::pruefeLaufzeiten($tageslaufzeit, $mindestlaufzeit, $sekundenBisTagesende);
|
||||||
|
|
||||||
|
$restlaufzeit = max(0, $mindestlaufzeit - $tageslaufzeit);
|
||||||
|
|
||||||
|
return $restlaufzeit > 0 && $sekundenBisTagesende <= $restlaufzeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sekundenBisNaechsteTagespruefung(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende,
|
||||||
|
bool $eingeschaltet
|
||||||
|
): int {
|
||||||
|
self::pruefeLaufzeiten($tageslaufzeit, $mindestlaufzeit, $sekundenBisTagesende);
|
||||||
|
|
||||||
|
$restlaufzeit = max(0, $mindestlaufzeit - $tageslaufzeit);
|
||||||
|
if ($restlaufzeit === 0) {
|
||||||
|
return max(1, $sekundenBisTagesende);
|
||||||
|
}
|
||||||
|
if ($eingeschaltet) {
|
||||||
|
return max(1, min($restlaufzeit, $sekundenBisTagesende));
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(1, $sekundenBisTagesende - $restlaufzeit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{Tag: string, Sekunden: int, StandZeit: int}
|
||||||
|
*/
|
||||||
|
public static function aktualisiereTageslaufzeit(
|
||||||
|
string $gespeicherterTag,
|
||||||
|
int $gespeicherteSekunden,
|
||||||
|
int $standZeit,
|
||||||
|
int $jetzt,
|
||||||
|
bool $warEingeschaltet,
|
||||||
|
DateTimeZone $zeitzone
|
||||||
|
): array {
|
||||||
|
if ($gespeicherteSekunden < 0 || $standZeit < 0 || $jetzt < 0) {
|
||||||
|
throw new InvalidArgumentException('Zeitwerte duerfen nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$jetztDatum = self::datum($jetzt, $zeitzone);
|
||||||
|
$aktuellerTag = $jetztDatum->format('Y-m-d');
|
||||||
|
if ($standZeit <= 0 || $standZeit > $jetzt) {
|
||||||
|
return [
|
||||||
|
'Tag' => $aktuellerTag,
|
||||||
|
'Sekunden' => $gespeicherterTag === $aktuellerTag ? $gespeicherteSekunden : 0,
|
||||||
|
'StandZeit' => $jetzt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$standDatum = self::datum($standZeit, $zeitzone);
|
||||||
|
$sekunden = $gespeicherteSekunden;
|
||||||
|
if ($gespeicherterTag !== $aktuellerTag || $standDatum->format('Y-m-d') !== $aktuellerTag) {
|
||||||
|
$sekunden = 0;
|
||||||
|
if ($warEingeschaltet) {
|
||||||
|
$tagesbeginn = $jetztDatum->setTime(0, 0)->getTimestamp();
|
||||||
|
$sekunden = max(0, $jetzt - max($standZeit, $tagesbeginn));
|
||||||
|
}
|
||||||
|
} elseif ($warEingeschaltet) {
|
||||||
|
$sekunden += $jetzt - $standZeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagesbeginn = $jetztDatum->setTime(0, 0);
|
||||||
|
$tagesdauer = $tagesbeginn->modify('+1 day')->getTimestamp() - $tagesbeginn->getTimestamp();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Tag' => $aktuellerTag,
|
||||||
|
'Sekunden' => min($sekunden, $tagesdauer),
|
||||||
|
'StandZeit' => $jetzt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sekundenBisTagesende(int $jetzt, DateTimeZone $zeitzone): int
|
||||||
|
{
|
||||||
|
if ($jetzt < 0) {
|
||||||
|
throw new InvalidArgumentException('Der Zeitpunkt darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$datum = self::datum($jetzt, $zeitzone);
|
||||||
|
$tagesende = $datum->setTime(0, 0)->modify('+1 day');
|
||||||
|
|
||||||
|
return max(1, $tagesende->getTimestamp() - $jetzt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pruefeLaufzeiten(
|
||||||
|
int $tageslaufzeit,
|
||||||
|
int $mindestlaufzeit,
|
||||||
|
int $sekundenBisTagesende
|
||||||
|
): void {
|
||||||
|
if ($tageslaufzeit < 0 || $mindestlaufzeit < 0 || $mindestlaufzeit > 86400) {
|
||||||
|
throw new InvalidArgumentException('Laufzeiten sind ausserhalb des gueltigen Bereichs.');
|
||||||
|
}
|
||||||
|
if ($sekundenBisTagesende <= 0) {
|
||||||
|
throw new InvalidArgumentException('Sekunden bis Tagesende muss groesser als 0 sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function datum(int $zeitpunkt, DateTimeZone $zeitzone): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return (new DateTimeImmutable('@' . $zeitpunkt))->setTimezone($zeitzone);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
return [
|
return [
|
||||||
['Batterie'],
|
['Batterie'],
|
||||||
['Pufferspeicher'],
|
['Pufferspeicher'],
|
||||||
['Verbraucher-1-Stufig'],
|
|
||||||
['Waermepumpe'],
|
['Waermepumpe'],
|
||||||
['Ladestation-Stand-Alone'],
|
['Ladestation-Stand-Alone'],
|
||||||
['Ladestation-Gateway'],
|
['Ladestation-Gateway'],
|
||||||
@@ -49,6 +48,15 @@ final class DokumentationsstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('Verteilalgorithmus', $inhalt);
|
self::assertStringContainsString('Verteilalgorithmus', $inhalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testVerbraucherEinStufigDokumentiertDenImplementiertenStand(): void
|
||||||
|
{
|
||||||
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Verbraucher-1-Stufig/README.md');
|
||||||
|
self::assertNotFalse($inhalt);
|
||||||
|
self::assertStringContainsString('Status: implementiert', $inhalt);
|
||||||
|
self::assertStringContainsString('Umschaltabstand', $inhalt);
|
||||||
|
self::assertStringContainsString('Installation und Inbetriebnahme', $inhalt);
|
||||||
|
}
|
||||||
|
|
||||||
public function testWarmwassererwaermerDokumentiertDenImplementiertenStand(): void
|
public function testWarmwassererwaermerDokumentiertDenImplementiertenStand(): void
|
||||||
{
|
{
|
||||||
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Wassererwaermer/README.md');
|
$inhalt = file_get_contents(__DIR__ . '/../docs/module/Wassererwaermer/README.md');
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use Belevo\EnelixEMS\EinStufigRegler;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class EinStufigReglerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFreiesLeistungsangebotEnthaeltAusUndEin(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[0, 3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGesperrtesOderErzwungenesAngebotEnthaeltNurIstleistung(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 3500, true, false, false)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[0],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 0, true, true, true)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
[3500],
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 3500, false, true, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUngueltigeIstleistungWirdAbgewiesen(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
EinStufigRegler::leistungsangebot(3500, 1200, true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMindestlaufzeitWirdErstZumSpaetestmoeglichenStartErzwungen(): void
|
||||||
|
{
|
||||||
|
self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 7200));
|
||||||
|
self::assertTrue(EinStufigRegler::mussMindestlaufzeitErzwingen(0, 3600, 3600));
|
||||||
|
self::assertTrue(EinStufigRegler::mussMindestlaufzeitErzwingen(1800, 3600, 1200));
|
||||||
|
self::assertFalse(EinStufigRegler::mussMindestlaufzeitErzwingen(3600, 3600, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNaechsteTagespruefungIstEreignisbasiert(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
3600,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(0, 3600, 7200, false)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
1800,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(1800, 3600, 7200, true)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
7200,
|
||||||
|
EinStufigRegler::sekundenBisNaechsteTagespruefung(3600, 3600, 7200, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLaufzeitWirdInnerhalbEinesTagesFortgeschrieben(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$von = (new DateTimeImmutable('2026-09-17 10:00:00', $zone))->getTimestamp();
|
||||||
|
$bis = (new DateTimeImmutable('2026-09-17 10:02:30', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['Tag' => '2026-09-17', 'Sekunden' => 250, 'StandZeit' => $bis],
|
||||||
|
EinStufigRegler::aktualisiereTageslaufzeit(
|
||||||
|
'2026-09-17',
|
||||||
|
100,
|
||||||
|
$von,
|
||||||
|
$bis,
|
||||||
|
true,
|
||||||
|
$zone
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLaufzeitWirdAnLokalerTagesgrenzeGetrennt(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$von = (new DateTimeImmutable('2026-09-16 23:59:50', $zone))->getTimestamp();
|
||||||
|
$bis = (new DateTimeImmutable('2026-09-17 00:00:10', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
['Tag' => '2026-09-17', 'Sekunden' => 10, 'StandZeit' => $bis],
|
||||||
|
EinStufigRegler::aktualisiereTageslaufzeit(
|
||||||
|
'2026-09-16',
|
||||||
|
1000,
|
||||||
|
$von,
|
||||||
|
$bis,
|
||||||
|
true,
|
||||||
|
$zone
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTagesendeBeruecksichtigtSommerzeit(): void
|
||||||
|
{
|
||||||
|
$zone = new DateTimeZone('Europe/Zurich');
|
||||||
|
$zeitpunkt = (new DateTimeImmutable('2026-03-29 00:00:00', $zone))->getTimestamp();
|
||||||
|
|
||||||
|
self::assertSame(23 * 3600, EinStufigRegler::sekundenBisTagesende($zeitpunkt, $zone));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Belevo\EnelixEMS\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class VerbraucherEinStufigModulstrukturTest extends TestCase
|
||||||
|
{
|
||||||
|
private const MODULPFAD = __DIR__ . '/../VerbraucherEinStufig/';
|
||||||
|
|
||||||
|
public function testModulIstVollstaendigUndMetadatenSindGueltig(): void
|
||||||
|
{
|
||||||
|
foreach (['module.json', 'form.json', 'module.php'] as $datei) {
|
||||||
|
self::assertFileExists(self::MODULPFAD . $datei);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadaten = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'module.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
self::assertSame('VerbraucherEinStufig', $metadaten['name']);
|
||||||
|
self::assertSame('Enelix', $metadaten['vendor']);
|
||||||
|
self::assertSame(['Verbraucher 1-Stufig'], $metadaten['aliases']);
|
||||||
|
self::assertSame(3, $metadaten['type']);
|
||||||
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPropertiesSindRegistriertUndImFormularVorhanden(): void
|
||||||
|
{
|
||||||
|
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
preg_match_all(
|
||||||
|
'/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/',
|
||||||
|
$basis . $modul,
|
||||||
|
$treffer
|
||||||
|
);
|
||||||
|
$properties = array_values(array_unique($treffer[1]));
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'PrioritaetPV',
|
||||||
|
'PrioritaetPeak',
|
||||||
|
'Meldeintervall',
|
||||||
|
'VorgabeTimeout',
|
||||||
|
'EinstellungenInVisu',
|
||||||
|
'LoggingEin',
|
||||||
|
'Nennleistung',
|
||||||
|
'SchaltkontaktVariableID',
|
||||||
|
'SchaltkontaktInvertiert',
|
||||||
|
'RueckmeldungVariableID',
|
||||||
|
'Mindestlaufzeit',
|
||||||
|
'Umschaltabstand',
|
||||||
|
'DiagnosevariablenAnzeigen',
|
||||||
|
],
|
||||||
|
$properties
|
||||||
|
);
|
||||||
|
|
||||||
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
self::assertStringContainsString('"name": "' . $property . '"', $formular, $property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNurUmschaltabstandErsetztAlteZyklusProperties(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('Umschaltabstand', 5)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('Interval'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('IdleCounterMax'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterTimer('Regelzyklus'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegelungReagiertAufEreignisseUndEinmalTimer(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('public function MessageSink', $modul);
|
||||||
|
self::assertStringContainsString('RegisterMessage($neueID, self::VM_UPDATE)', $modul);
|
||||||
|
foreach (['Umschaltfreigabe', 'VorgabeTimeout', 'Tagesplanung'] as $timer) {
|
||||||
|
self::assertStringContainsString("'$timer'", $modul);
|
||||||
|
}
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"SetTimerInterval('Meldezyklus', \$this->ReadPropertyInteger('Meldeintervall') * 1000)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testModulVerwendetAusschliesslichDenVertrag30(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString('implements VerbraucherSchnittstelle', $modul);
|
||||||
|
self::assertStringContainsString('ManagerdatenEmpfangen(array $daten)', $modul);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeManagerdaten', $modul);
|
||||||
|
self::assertStringContainsString('Nachrichtenvertrag::pruefeVerbraucherdaten', $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterVariableString('PowerSteps'", $modul);
|
||||||
|
self::assertStringNotContainsString("RegisterPropertyInteger('ManagerID'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFachvariablenUndRueckmeldestoerungSindEnthalten(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterVariableBoolean('Schaltzustand'",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterVariableInteger('Tageslaufzeit'",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Rueckmeldefehler'", $modul);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Tageslaufzeit_s'", $modul);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiagnosevariablenSindOptional(): void
|
||||||
|
{
|
||||||
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|
||||||
|
self::assertStringContainsString("ReadPropertyBoolean('DiagnosevariablenAnzeigen')", $modul);
|
||||||
|
self::assertStringContainsString('registriereVerbraucherDiagnose', $modul);
|
||||||
|
self::assertStringContainsString('UnregisterVariable($ident)', $modul);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user