Compare commits
8
Commits
38353aed93
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f91e4319 | ||
|
|
ec7182b833 | ||
|
|
37addf4242 | ||
|
|
8d83b083bf | ||
|
|
6cd7bf19f5 | ||
|
|
3ce9af4244 | ||
|
|
8357e8dfab | ||
|
|
cf92e7623b |
+11
-4
@@ -82,19 +82,26 @@ Messwert, ist er veraltet oder liegt der Ladezustand ausserhalb von 0 bis
|
||||
ReserveLadezustand entspricht der bisherigen Peakshaving-Reserve.
|
||||
MindestLadezustand schuetzt vor weiterer Entladung.
|
||||
LadezustandHysterese ersetzt die bisher fest im Code hinterlegte
|
||||
2-Prozent-Hysterese.
|
||||
2-Prozent-Hysterese. NachladenMitMaximalleistung verwendet standardmaessig die
|
||||
dynamische maximale Ladeleistung. Wird der Schalter deaktiviert, begrenzt
|
||||
MaximaleNachladeleistung das schutzbedingte Nachladen in W. Der wirksame Wert
|
||||
wird nie hoeher als die dynamische Ladegrenze der Batterie.
|
||||
|
||||
Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
|
||||
|
||||
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
|
||||
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung.
|
||||
- Unterhalb der Reserve wird die maximale Ladeleistung als fester Wert angeboten.
|
||||
- Unterhalb der Reserve wird die wirksame Nachladeleistung als fester Wert
|
||||
angeboten.
|
||||
- Bei vollem Speicher wird der durchgaengige Bereich von der maximalen
|
||||
Entladeleistung bis 0 W angeboten.
|
||||
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
|
||||
Batterieleistung berechnete Entladewert angeboten.
|
||||
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden
|
||||
Laden und Entladen wie bisher eingeschraenkt.
|
||||
Laden und Entladen wie bisher eingeschraenkt; die positive Grenze ist dabei
|
||||
hoechstens die konfigurierte Nachladeleistung.
|
||||
- Oberhalb der Reserve ohne aktive Hysterese bleibt der normale Ladebereich
|
||||
unveraendert und wird nicht durch die Nachladegrenze reduziert.
|
||||
|
||||
## Manager und Zeitverhalten
|
||||
|
||||
@@ -133,7 +140,7 @@ verwirft die aktuelle Vorgabe und schreibt eine Leistung von 0.
|
||||
1. Batterietyp und Steuerung auswaehlen.
|
||||
2. Alle fuenf Messwertvariablen zuordnen.
|
||||
3. Die automatisch eingeblendeten Registervariablen zuordnen.
|
||||
4. Reserve, Mindestladezustand und Hysterese pruefen.
|
||||
4. Reserve, Mindestladezustand, Hysterese und Nachladeleistung pruefen.
|
||||
5. Diagnosevariablen und Logging fuer die Erstinbetriebnahme aktivieren.
|
||||
6. Die Batterie im Manager zuordnen.
|
||||
7. Aktiv einschalten.
|
||||
|
||||
@@ -137,6 +137,20 @@
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"digits": 1
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "NachladenMitMaximalleistung",
|
||||
"caption": "Beim Nachladen maximale Ladeleistung verwenden",
|
||||
"onChange": "IPS_RequestAction($id, 'FormNachladenMitMaximalleistung', $NachladenMitMaximalleistung);"
|
||||
},
|
||||
{
|
||||
"type": "NumberSpinner",
|
||||
"name": "MaximaleNachladeleistung",
|
||||
"caption": "Maximale Nachladeleistung",
|
||||
"suffix": " W",
|
||||
"minimum": 1,
|
||||
"visible": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+47
-2
@@ -86,6 +86,8 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->RegisterPropertyFloat('ReserveLadezustand', 20.0);
|
||||
$this->RegisterPropertyFloat('MindestLadezustand', 10.0);
|
||||
$this->RegisterPropertyFloat('LadezustandHysterese', 2.0);
|
||||
$this->RegisterPropertyBoolean('NachladenMitMaximalleistung', true);
|
||||
$this->RegisterPropertyInteger('MaximaleNachladeleistung', 0);
|
||||
$this->RegisterPropertyInteger('MesswertMaxAlter', 60);
|
||||
$this->RegisterPropertyInteger('Aenderungssperre', 4);
|
||||
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
|
||||
@@ -185,6 +187,11 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$formular['elements'],
|
||||
$this->ReadPropertyInteger('Batterietyp')
|
||||
);
|
||||
$this->setzeFormfeldSichtbar(
|
||||
$formular['elements'],
|
||||
'MaximaleNachladeleistung',
|
||||
!$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
|
||||
);
|
||||
|
||||
return json_encode($formular, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
@@ -251,6 +258,14 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->aktualisiereFormRegisterfelder((int) $wert);
|
||||
return;
|
||||
|
||||
case 'FormNachladenMitMaximalleistung':
|
||||
$this->UpdateFormField(
|
||||
'MaximaleNachladeleistung',
|
||||
'visible',
|
||||
!(bool) $wert
|
||||
);
|
||||
return;
|
||||
|
||||
case 'ManagerdatenEmpfangen':
|
||||
if (!is_string($wert)) {
|
||||
throw new InvalidArgumentException(
|
||||
@@ -382,7 +397,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$messwerte['Netzleistung'],
|
||||
$messwerte['Istleistung'],
|
||||
$messwerte['MaxLaden'],
|
||||
$messwerte['MaxEntladen']
|
||||
$messwerte['MaxEntladen'],
|
||||
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
|
||||
? null
|
||||
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
|
||||
)
|
||||
: [0];
|
||||
|
||||
@@ -776,6 +794,14 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
|
||||
&& $this->ReadPropertyInteger('MaximaleNachladeleistung') <= 0
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
'MaximaleNachladeleistung muss groesser als 0 W sein.'
|
||||
);
|
||||
}
|
||||
|
||||
BatterieRegler::hystereseAktiv(
|
||||
50.0,
|
||||
$this->ReadPropertyFloat('ReserveLadezustand'),
|
||||
@@ -793,7 +819,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0
|
||||
1.0,
|
||||
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
|
||||
? null
|
||||
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
|
||||
);
|
||||
|
||||
foreach (self::MESSWERT_PROPERTIES as $property) {
|
||||
@@ -1096,6 +1125,22 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->setzeZustand('Stoertext', $text);
|
||||
}
|
||||
|
||||
/** @param array<int, mixed> $elemente */
|
||||
private function setzeFormfeldSichtbar(
|
||||
array &$elemente,
|
||||
string $name,
|
||||
bool $sichtbar
|
||||
): void {
|
||||
foreach ($elemente as &$element) {
|
||||
if (($element['name'] ?? '') === $name) {
|
||||
$element['visible'] = $sichtbar;
|
||||
}
|
||||
if (isset($element['items']) && is_array($element['items'])) {
|
||||
$this->setzeFormfeldSichtbar($element['items'], $name, $sichtbar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, mixed> $elemente */
|
||||
private function setzeRegisterfelderSichtbar(
|
||||
array &$elemente,
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Manager: Positives Budget wird bei gleicher Prioritaet nach der kleinsten
|
||||
naechsten erreichbaren Sollleistung verteilt. Nur bei gleicher Leistungsstufe
|
||||
entscheiden exakter Energiebezug und anschliessend die Instanz-ID. Einzelstufen
|
||||
und ganzzahlige Bereiche teilen sich das Budget; reservierte Lasten und die
|
||||
negative Defizitregelung bleiben unveraendert. Keine Konfigurationsmigration.
|
||||
|
||||
## 0.1 Build 1 - Erste Beta - 2026-09-29
|
||||
|
||||
### Enthalten
|
||||
@@ -15,7 +23,10 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
|
||||
|
||||
### Beta-Aenderungen
|
||||
|
||||
- Ladestationen halten beim Aktivieren und Umschalten von Solarladen den bisherigen Strom bis zur neuen Manager-Vorgabe; die Stand-alone-Phasenprobe verwendet nur noch 6 A.
|
||||
- Fahrzeugstatus ist bei beiden Ladestationsvarianten eine Diagnosevariable, Ist- und Sollleistung gehoeren zu den optionalen Visualisierungseinstellungen.
|
||||
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
|
||||
- Schutzbedingtes Nachladen der Batterie kann wahlweise auf die dynamische Maximalleistung oder eine benutzerdefinierte Watt-Grenze begrenzt werden.
|
||||
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
|
||||
- Stand-alone-Ladestationen erkennen nach einer einstellbaren Beobachtungszeit den niedrigeren Fahrzeug-Maximalstrom und begrenzen ihr Leistungsangebot ohne Ladeunterbruch.
|
||||
- Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
|
||||
|
||||
+119
-59
@@ -28,17 +28,17 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
private const STATUS_GATEWAY_GETRENNT = 202;
|
||||
private const STATUS_WARTET_AUF_DATEN = 203;
|
||||
private const STATUS_LADEFEHLER = 204;
|
||||
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
|
||||
|
||||
/** @var list<string> */
|
||||
private const DIAGNOSE_VARIABLEN = [
|
||||
'Istleistung',
|
||||
'Leistungsquelle',
|
||||
'Sollleistung',
|
||||
'SollwertGueltig',
|
||||
'Verfuegbar',
|
||||
'AenderungMoeglich',
|
||||
'Stoerung',
|
||||
'Stoertext',
|
||||
'Fahrzeugstatus',
|
||||
'GatewayVerbunden',
|
||||
'ApiMaximalstrom',
|
||||
'EaseeAktiviert',
|
||||
@@ -75,7 +75,6 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
'~Switch',
|
||||
110
|
||||
);
|
||||
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
|
||||
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
|
||||
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
|
||||
|
||||
@@ -113,6 +112,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
|
||||
$this->RegisterAttributeString('LetzterGeraetebefehl', '');
|
||||
$this->RegisterAttributeString('LetzterStartbefehl', '');
|
||||
$this->RegisterAttributeBoolean('RegeluebergangAktiv', false);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'Meldezyklus',
|
||||
@@ -129,6 +129,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RegeluebergangTimeout',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
|
||||
);
|
||||
|
||||
$this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID);
|
||||
}
|
||||
@@ -142,6 +147,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->WriteAttributeBoolean('GatewayVerbunden', false);
|
||||
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
|
||||
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
|
||||
$this->beendeRegeluebergang();
|
||||
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
@@ -167,17 +173,27 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'Aktiv':
|
||||
$this->SetValue('Aktiv', (bool) $wert);
|
||||
if (!(bool) $wert) {
|
||||
$aktiv = (bool) $wert;
|
||||
$this->SetValue('Aktiv', $aktiv);
|
||||
if ($aktiv) {
|
||||
$this->starteRegeluebergang();
|
||||
} else {
|
||||
$this->beendeRegeluebergang();
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
}
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Ladefreigabe':
|
||||
$this->WriteAttributeBoolean('ZustandLadefreigabe', (bool) $wert);
|
||||
$this->setzeSichtbareVariable('Ladefreigabe', (bool) $wert);
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
$ladefreigabe = (bool) $wert;
|
||||
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
|
||||
$this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
|
||||
if ($ladefreigabe) {
|
||||
$this->starteRegeluebergang();
|
||||
} else {
|
||||
$this->beendeRegeluebergang();
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
}
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
@@ -185,9 +201,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$solarladen = EaseeLadestatus::solarladenErzwungen(
|
||||
$this->ReadPropertyInteger('Betriebsmodus')
|
||||
) ? true : (bool) $wert;
|
||||
if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
|
||||
$this->starteRegeluebergang();
|
||||
}
|
||||
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
|
||||
$this->setzeSichtbareVariable('Solarladen', $solarladen);
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
@@ -201,6 +219,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'RegeluebergangTimeout':
|
||||
$this->beendeRegeluebergang();
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Melden':
|
||||
if ((bool) $wert) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
@@ -288,7 +311,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||
$this->aktualisiere(false);
|
||||
$this->aktualisiere(false, false);
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if ($sollleistung === null) {
|
||||
if (
|
||||
@@ -311,6 +334,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
);
|
||||
}
|
||||
|
||||
$this->beendeRegeluebergang();
|
||||
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||
$this->setzeZustand('Sollleistung', $sollleistung);
|
||||
@@ -405,7 +429,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->WriteAttributeInteger('LetzteBeobachtung', $zeitpunkt);
|
||||
}
|
||||
|
||||
private function aktualisiere(bool $meldungPlanen): void
|
||||
private function aktualisiere(
|
||||
bool $meldungPlanen,
|
||||
bool $steuerungAusfuehren = true
|
||||
): void
|
||||
{
|
||||
try {
|
||||
$status = EaseeLadestatus::ausBeobachtungen(
|
||||
@@ -493,12 +520,21 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
}
|
||||
$this->setzeStoerung($stoertext);
|
||||
|
||||
$regeluebergangAktiv = (bool) $this->GetValue('Aktiv')
|
||||
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
|
||||
&& $steuerbar
|
||||
&& $status['FahrzeugVerbunden']
|
||||
&& !$status['FahrzeugGeladen']
|
||||
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
|
||||
if ($gatewayVerbunden && $status['StatusGueltig']) {
|
||||
$this->setzeLadestrom(
|
||||
$this->bestimmeWirksameSollleistung($angebot),
|
||||
$regelPhasenzahl,
|
||||
$status['Fahrzeugstatus'] === 2
|
||||
);
|
||||
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
||||
if ($steuerungAusfuehren && !$regeluebergangAktiv) {
|
||||
$this->setzeLadestrom(
|
||||
$sollleistung,
|
||||
$regelPhasenzahl,
|
||||
$status['Fahrzeugstatus'] === 2
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
|
||||
}
|
||||
@@ -797,6 +833,23 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
return is_array($zuordnung) ? $zuordnung : [];
|
||||
}
|
||||
|
||||
private function starteRegeluebergang(): void
|
||||
{
|
||||
if ($this->zugeordneteManagerIDs() === []) {
|
||||
$this->beendeRegeluebergang();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->WriteAttributeBoolean('RegeluebergangAktiv', true);
|
||||
$this->SetTimerInterval('RegeluebergangTimeout', self::REGELUEBERGANG_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private function beendeRegeluebergang(): void
|
||||
{
|
||||
$this->WriteAttributeBoolean('RegeluebergangAktiv', false);
|
||||
$this->SetTimerInterval('RegeluebergangTimeout', 0);
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||
@@ -857,95 +910,63 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
|
||||
private function aktualisiereVariablen(): void
|
||||
{
|
||||
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||
$this->RegisterVariableBoolean(
|
||||
'Ladefreigabe',
|
||||
'Ladefreigabe',
|
||||
'~Switch',
|
||||
150
|
||||
);
|
||||
$this->RegisterVariableBoolean(
|
||||
'Solarladen',
|
||||
'Solarladen',
|
||||
'~Switch',
|
||||
160
|
||||
);
|
||||
$this->EnableAction('Ladefreigabe');
|
||||
$this->EnableAction('Solarladen');
|
||||
$this->SetValue(
|
||||
'Ladefreigabe',
|
||||
$this->ReadAttributeBoolean('ZustandLadefreigabe')
|
||||
);
|
||||
$this->SetValue(
|
||||
'Solarladen',
|
||||
$this->ReadAttributeBoolean('ZustandSolarladen')
|
||||
);
|
||||
} else {
|
||||
$this->entferneVariable('Ladefreigabe');
|
||||
$this->entferneVariable('Solarladen');
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||
$this->registriereVerbraucherDiagnose();
|
||||
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
|
||||
$this->RegisterVariableBoolean(
|
||||
'GatewayVerbunden',
|
||||
'Easee Gateway verbunden',
|
||||
'~Switch',
|
||||
200
|
||||
);
|
||||
$this->RegisterVariableInteger(
|
||||
'ApiMaximalstrom',
|
||||
'API-Maximalstrom',
|
||||
'',
|
||||
210
|
||||
);
|
||||
$this->RegisterVariableInteger('ApiMaximalstrom', 'API-Maximalstrom', '', 220);
|
||||
$this->RegisterVariableBoolean(
|
||||
'EaseeAktiviert',
|
||||
'Easee aktiviert',
|
||||
'~Switch',
|
||||
220
|
||||
230
|
||||
);
|
||||
$this->RegisterVariableBoolean(
|
||||
'AutorisierungErforderlich',
|
||||
'Autorisierung erforderlich',
|
||||
'~Switch',
|
||||
230
|
||||
240
|
||||
);
|
||||
$this->RegisterVariableBoolean(
|
||||
'RemoteStartErforderlich',
|
||||
'Remote-Start erforderlich',
|
||||
'~Switch',
|
||||
240
|
||||
250
|
||||
);
|
||||
$this->RegisterVariableBoolean(
|
||||
'SmartCharging',
|
||||
'Easee Smart Charging',
|
||||
'~Switch',
|
||||
250
|
||||
260
|
||||
);
|
||||
$this->RegisterVariableInteger(
|
||||
'GrundKeinLadestrom',
|
||||
'Grund kein Ladestrom',
|
||||
'',
|
||||
260
|
||||
270
|
||||
);
|
||||
$this->RegisterVariableString(
|
||||
'LetzterGeraetebefehl',
|
||||
'Letzter Geraetebefehl',
|
||||
'',
|
||||
270
|
||||
280
|
||||
);
|
||||
$this->RegisterVariableString(
|
||||
'LetzterStartbefehl',
|
||||
'Letzter Startbefehl',
|
||||
'',
|
||||
280
|
||||
290
|
||||
);
|
||||
$this->RegisterVariableString(
|
||||
'LeistungsangebotDiagnose',
|
||||
'Leistungsangebot',
|
||||
'',
|
||||
290
|
||||
300
|
||||
);
|
||||
$this->SetValue(
|
||||
'LeistungsangebotDiagnose',
|
||||
@@ -983,6 +1004,40 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
$this->entferneVariable($ident);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||
$this->RegisterVariableBoolean(
|
||||
'Ladefreigabe',
|
||||
'Ladefreigabe',
|
||||
'~Switch',
|
||||
150
|
||||
);
|
||||
$this->RegisterVariableBoolean(
|
||||
'Solarladen',
|
||||
'Solarladen',
|
||||
'~Switch',
|
||||
160
|
||||
);
|
||||
$this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 170);
|
||||
$this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180);
|
||||
$this->EnableAction('Ladefreigabe');
|
||||
$this->EnableAction('Solarladen');
|
||||
$this->SetValue(
|
||||
'Ladefreigabe',
|
||||
$this->ReadAttributeBoolean('ZustandLadefreigabe')
|
||||
);
|
||||
$this->SetValue(
|
||||
'Solarladen',
|
||||
$this->ReadAttributeBoolean('ZustandSolarladen')
|
||||
);
|
||||
$this->SetValue('Istleistung', $this->ReadAttributeFloat('ZustandIstleistung'));
|
||||
$this->SetValue('Sollleistung', $this->ReadAttributeInteger('ZustandSollleistung'));
|
||||
} else {
|
||||
$this->entferneVariable('Ladefreigabe');
|
||||
$this->entferneVariable('Solarladen');
|
||||
$this->entferneVariable('Istleistung');
|
||||
$this->entferneVariable('Sollleistung');
|
||||
}
|
||||
}
|
||||
|
||||
private function entferneVariable(string $ident): void
|
||||
@@ -1108,7 +1163,12 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
||||
|
||||
private function deaktiviereTimer(): void
|
||||
{
|
||||
foreach (['Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
|
||||
foreach ([
|
||||
'Meldezyklus',
|
||||
'RueckmeldungVerzoegert',
|
||||
'VorgabeTimeout',
|
||||
'RegeluebergangTimeout',
|
||||
] as $timer) {
|
||||
$this->SetTimerInterval($timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,18 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
private const STATUS_AKTIV = 102;
|
||||
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
|
||||
private const STATUS_KOMMUNIKATIONSFEHLER = 202;
|
||||
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
|
||||
private const PHASENERKENNUNGSSTROM = 6;
|
||||
|
||||
/** @var list<string> */
|
||||
private const DIAGNOSE_VARIABLEN = [
|
||||
'Istleistung',
|
||||
'Leistungsquelle',
|
||||
'Sollleistung',
|
||||
'SollwertGueltig',
|
||||
'Verfuegbar',
|
||||
'AenderungMoeglich',
|
||||
'Stoerung',
|
||||
'Stoertext',
|
||||
'Fahrzeugstatus',
|
||||
'FahrzeugMaximalstrom',
|
||||
'LetzterGeraetebefehl',
|
||||
'LeistungsangebotDiagnose',
|
||||
@@ -67,7 +68,6 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
|
||||
$this->RegisterVariableBoolean('FahrzeugVerbunden', 'Fahrzeug verbunden', '~Switch', 100);
|
||||
$this->RegisterVariableBoolean('FahrzeugGeladen', 'Fahrzeug geladen', '~Switch', 110);
|
||||
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
|
||||
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
|
||||
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
|
||||
|
||||
@@ -100,9 +100,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->RegisterAttributeInteger('PhasenerkennungSeit', 0);
|
||||
$this->RegisterAttributeInteger('PhasenerkennungVersion', 0);
|
||||
$this->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||
$this->RegisterAttributeInteger('GeraeteMaximalstrom', 32);
|
||||
$this->RegisterAttributeInteger('ErkannterMaximalstrom', 32);
|
||||
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
|
||||
$this->RegisterAttributeInteger('FahrzeugstromKandidatSeit', 0);
|
||||
$this->RegisterAttributeBoolean('RegeluebergangAktiv', false);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'Statuszyklus',
|
||||
@@ -124,6 +126,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RegeluebergangTimeout',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
@@ -154,6 +161,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->ReadPropertyInteger('MaximalerLadestrom')
|
||||
);
|
||||
$this->setzeFahrzeugstromKandidatZurueck();
|
||||
$this->beendeRegeluebergang();
|
||||
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
@@ -176,18 +184,37 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'Aktiv':
|
||||
$this->SetValue('Aktiv', (bool) $wert);
|
||||
if (!(bool) $wert) {
|
||||
$aktiv = (bool) $wert;
|
||||
$this->SetValue('Aktiv', $aktiv);
|
||||
if ($aktiv) {
|
||||
$this->starteRegeluebergang();
|
||||
} else {
|
||||
$this->beendeRegeluebergang();
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
}
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Ladefreigabe':
|
||||
$ladefreigabe = (bool) $wert;
|
||||
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
|
||||
$this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
|
||||
if ($ladefreigabe) {
|
||||
$this->starteRegeluebergang();
|
||||
} else {
|
||||
$this->beendeRegeluebergang();
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
}
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Solarladen':
|
||||
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert);
|
||||
$this->setzeSichtbareVariable($ident, (bool) $wert);
|
||||
$this->setzeZustand('SollwertGueltig', false);
|
||||
$solarladen = (bool) $wert;
|
||||
if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
|
||||
$this->starteRegeluebergang();
|
||||
}
|
||||
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
|
||||
$this->setzeSichtbareVariable('Solarladen', $solarladen);
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
@@ -201,11 +228,15 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'RegeluebergangTimeout':
|
||||
$this->beendeRegeluebergang();
|
||||
$this->aktualisiere(true);
|
||||
return;
|
||||
|
||||
case 'Melden':
|
||||
if ((bool) $wert) {
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
|
||||
}
|
||||
$this->aktualisiere(false);
|
||||
$this->sendeVerbraucherdaten();
|
||||
return;
|
||||
|
||||
@@ -238,7 +269,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
}
|
||||
|
||||
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
|
||||
$this->aktualisiere(false, false);
|
||||
$this->aktualisiere(false, false, false);
|
||||
$sollleistung = $daten['Sollleistung_W'];
|
||||
if ($sollleistung === null) {
|
||||
if ((bool) $this->leseZustand('SollwertGueltig')
|
||||
@@ -256,6 +287,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
);
|
||||
}
|
||||
|
||||
$this->beendeRegeluebergang();
|
||||
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
|
||||
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
|
||||
$this->setzeZustand('Sollleistung', $sollleistung);
|
||||
@@ -265,7 +297,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
|
||||
}
|
||||
|
||||
private function aktualisiere(bool $meldungPlanen, bool $statusLesen = true): void
|
||||
private function aktualisiere(
|
||||
bool $meldungPlanen,
|
||||
bool $statusLesen = true,
|
||||
bool $steuerungAusfuehren = true
|
||||
): void
|
||||
{
|
||||
try {
|
||||
if ($statusLesen) {
|
||||
@@ -279,6 +315,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
|
||||
$maximalstrom = min(
|
||||
$this->ReadPropertyInteger('MaximalerLadestrom'),
|
||||
max(6, $this->ReadAttributeInteger('GeraeteMaximalstrom')),
|
||||
max(6, $this->ReadAttributeInteger('ErkannterMaximalstrom'))
|
||||
);
|
||||
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $maximalstrom);
|
||||
@@ -346,12 +383,17 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
|
||||
|
||||
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
||||
if (!($aktiv
|
||||
$phasenerkennungAktiv = $aktiv
|
||||
&& $ladefreigabe
|
||||
&& $verbunden
|
||||
&& !$geladen
|
||||
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
|
||||
)) {
|
||||
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT;
|
||||
$regeluebergangAktiv = $aktiv
|
||||
&& $ladefreigabe
|
||||
&& $verbunden
|
||||
&& !$geladen
|
||||
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
|
||||
if ($steuerungAusfuehren && !$phasenerkennungAktiv && !$regeluebergangAktiv) {
|
||||
$this->setzeLadestrom($sollleistung);
|
||||
}
|
||||
$this->setzeStoerung('');
|
||||
@@ -382,6 +424,15 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->ReadPropertyInteger('Geraetetyp'),
|
||||
$antwort
|
||||
);
|
||||
if (isset($rohstatus['Sollstrom_A'])) {
|
||||
$geraeteSollstrom = (int) $rohstatus['Sollstrom_A'];
|
||||
if ($geraeteSollstrom === 0 || ($geraeteSollstrom >= 6 && $geraeteSollstrom <= 32)) {
|
||||
$this->WriteAttributeInteger('LetzterGesetzterStrom', $geraeteSollstrom);
|
||||
}
|
||||
}
|
||||
if (isset($rohstatus['Maximalstrom_A'])) {
|
||||
$this->WriteAttributeInteger('GeraeteMaximalstrom', (int) $rohstatus['Maximalstrom_A']);
|
||||
}
|
||||
|
||||
$warVerbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
|
||||
$verbunden = $rohstatus['Rohstatus'] !== 1;
|
||||
@@ -413,17 +464,16 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
) {
|
||||
$erkennungSeit = $this->ReadAttributeInteger('PhasenerkennungSeit');
|
||||
if ($erkennungSeit <= 0) {
|
||||
$this->sendeLadestromAnGeraet(
|
||||
$this->ReadPropertyInteger('MaximalerLadestrom')
|
||||
);
|
||||
$this->sendeLadestromAnGeraet(self::PHASENERKENNUNGSSTROM);
|
||||
$erkennungSeit = time();
|
||||
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
|
||||
} elseif ((time() - $erkennungSeit)
|
||||
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
|
||||
) {
|
||||
$bekanntePhasenzahl = $rohstatus['Istleistung_W'] > 7500.0
|
||||
? LadestationRegler::PHASEN_DREI
|
||||
: LadestationRegler::PHASEN_EIN;
|
||||
$bekanntePhasenzahl = LadestationRegler::erkennePhasenzahlAusLeistung(
|
||||
(float) $rohstatus['Istleistung_W'],
|
||||
$this->ReadAttributeInteger('LetzterGesetzterStrom')
|
||||
);
|
||||
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||
}
|
||||
} else {
|
||||
@@ -819,6 +869,23 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
return is_array($zuordnung) ? $zuordnung : [];
|
||||
}
|
||||
|
||||
private function starteRegeluebergang(): void
|
||||
{
|
||||
if ($this->zugeordneteManagerIDs() === []) {
|
||||
$this->beendeRegeluebergang();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->WriteAttributeBoolean('RegeluebergangAktiv', true);
|
||||
$this->SetTimerInterval('RegeluebergangTimeout', self::REGELUEBERGANG_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private function beendeRegeluebergang(): void
|
||||
{
|
||||
$this->WriteAttributeBoolean('RegeluebergangAktiv', false);
|
||||
$this->SetTimerInterval('RegeluebergangTimeout', 0);
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
|
||||
@@ -882,27 +949,16 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
|
||||
private function aktualisiereVariablen(): void
|
||||
{
|
||||
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||
$this->RegisterVariableBoolean('Ladefreigabe', 'Ladefreigabe', '~Switch', 150);
|
||||
$this->RegisterVariableBoolean('Solarladen', 'Solarladen', '~Switch', 160);
|
||||
$this->EnableAction('Ladefreigabe');
|
||||
$this->EnableAction('Solarladen');
|
||||
$this->SetValue('Ladefreigabe', $this->ReadAttributeBoolean('ZustandLadefreigabe'));
|
||||
$this->SetValue('Solarladen', $this->ReadAttributeBoolean('ZustandSolarladen'));
|
||||
} else {
|
||||
$this->entferneVariable('Ladefreigabe');
|
||||
$this->entferneVariable('Solarladen');
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
|
||||
$this->registriereVerbraucherDiagnose();
|
||||
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200);
|
||||
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210);
|
||||
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
|
||||
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 210);
|
||||
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 220);
|
||||
$this->RegisterVariableInteger(
|
||||
'FahrzeugMaximalstrom',
|
||||
'Erkannter Fahrzeug-Maximalstrom',
|
||||
'',
|
||||
220
|
||||
230
|
||||
);
|
||||
$this->SetValue(
|
||||
'FahrzeugMaximalstrom',
|
||||
@@ -926,6 +982,24 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->entferneVariable($ident);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
|
||||
$this->RegisterVariableBoolean('Ladefreigabe', 'Ladefreigabe', '~Switch', 150);
|
||||
$this->RegisterVariableBoolean('Solarladen', 'Solarladen', '~Switch', 160);
|
||||
$this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 170);
|
||||
$this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180);
|
||||
$this->EnableAction('Ladefreigabe');
|
||||
$this->EnableAction('Solarladen');
|
||||
$this->SetValue('Ladefreigabe', $this->ReadAttributeBoolean('ZustandLadefreigabe'));
|
||||
$this->SetValue('Solarladen', $this->ReadAttributeBoolean('ZustandSolarladen'));
|
||||
$this->SetValue('Istleistung', $this->ReadAttributeFloat('ZustandIstleistung'));
|
||||
$this->SetValue('Sollleistung', $this->ReadAttributeInteger('ZustandSollleistung'));
|
||||
} else {
|
||||
$this->entferneVariable('Ladefreigabe');
|
||||
$this->entferneVariable('Solarladen');
|
||||
$this->entferneVariable('Istleistung');
|
||||
$this->entferneVariable('Sollleistung');
|
||||
}
|
||||
}
|
||||
|
||||
private function entferneVariable(string $ident): void
|
||||
@@ -1033,7 +1107,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
|
||||
private function deaktiviereTimer(): void
|
||||
{
|
||||
foreach (['Statuszyklus', 'Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
|
||||
foreach ([
|
||||
'Statuszyklus',
|
||||
'Meldezyklus',
|
||||
'RueckmeldungVerzoegert',
|
||||
'VorgabeTimeout',
|
||||
'RegeluebergangTimeout',
|
||||
] as $timer) {
|
||||
$this->SetTimerInterval($timer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+52
-9
@@ -260,10 +260,14 @@
|
||||
"type": "Label",
|
||||
"caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden."
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Die Wechselrichter-Tabelle beschreibt die AC-Wandler der Anlage. PV-Flaechen und Batteriespeicher verweisen darauf, damit gemeinsame Leistungsgrenzen und die elektrische Kopplung eindeutig sind. Bei einem Hybridgeraet verwenden PV und Batterie dieselbe Wechselrichter-ID; bei einem AC-gekoppelten Speicher wird dessen separater Batteriewechselrichter erfasst."
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenWechselrichter",
|
||||
"caption": "Wechselrichter",
|
||||
"caption": "Wechselrichter / AC-Wandler",
|
||||
"rowCount": 4,
|
||||
"add": true,
|
||||
"delete": true,
|
||||
@@ -459,6 +463,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Der Batterieeintrag beschreibt den Speicher selbst: Kapazitaet, Leistung, Ladezustand und Energiezaehler. Die Wechselrichter-ID ist nur die Verknuepfung zum zugehoerigen AC-Wandler und keine doppelte Batteriekonfiguration."
|
||||
},
|
||||
{
|
||||
"type": "List",
|
||||
"name": "AnlagenBatterien",
|
||||
@@ -722,26 +730,61 @@
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergyPieAnzeigen",
|
||||
"caption": "Energy Pie unter dem Manager anzeigen",
|
||||
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
|
||||
"caption": "Energy Pie unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergiediagrammeAnzeigen",
|
||||
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen",
|
||||
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);"
|
||||
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "RowLayout",
|
||||
"items": [
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "DiagrammPVModus",
|
||||
"caption": "PV in Diagrammen",
|
||||
"options": [
|
||||
{"caption": "Summe", "value": 0},
|
||||
{"caption": "Einzelne Anlagen", "value": 1},
|
||||
{"caption": "Summe und einzelne Anlagen", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
"name": "DiagrammBatterieModus",
|
||||
"caption": "Batterien in Diagrammen",
|
||||
"options": [
|
||||
{"caption": "Summe", "value": 0},
|
||||
{"caption": "Einzelne Speicher", "value": 1},
|
||||
{"caption": "Summe und einzelne Speicher", "value": 2}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "LadestationenSeparatAnzeigen",
|
||||
"caption": "Ladestationen separat in Energiefluss und Diagrammen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "VerbraucherSeparatAnzeigen",
|
||||
"caption": "Uebrige Verbraucher separat in Energiefluss und Diagrammen anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"caption": "Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert. Fuer eine Neuerzeugung den jeweiligen Schalter ausschalten, Apply ausfuehren, wieder einschalten und erneut Apply ausfuehren."
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "FunFactsAnzeigen",
|
||||
"caption": "Energy Facts unter dem Manager anzeigen",
|
||||
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
|
||||
"caption": "Energy Facts unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "CheckBox",
|
||||
"name": "EnergieflussAnzeigen",
|
||||
"caption": "Energiefluss unter dem Manager anzeigen",
|
||||
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
|
||||
"caption": "Energiefluss unter dem Manager anzeigen"
|
||||
},
|
||||
{
|
||||
"type": "Select",
|
||||
|
||||
+75
-11
@@ -171,6 +171,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
|
||||
$this->RegisterPropertyBoolean('EnergyPieAnzeigen', false);
|
||||
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
|
||||
$this->RegisterPropertyInteger('DiagrammPVModus', 0);
|
||||
$this->RegisterPropertyInteger('DiagrammBatterieModus', 0);
|
||||
$this->RegisterPropertyBoolean('LadestationenSeparatAnzeigen', true);
|
||||
$this->RegisterPropertyBoolean('VerbraucherSeparatAnzeigen', true);
|
||||
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
|
||||
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
|
||||
$this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0);
|
||||
@@ -203,6 +207,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
|
||||
$this->RegisterAttributeString('MessungEnergiePuffer', '{}');
|
||||
$this->RegisterAttributeString('MessungZaehlerstaende', '{}');
|
||||
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
|
||||
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
|
||||
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
|
||||
|
||||
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
|
||||
$this->RegisterTimer('Regelzyklus', 0, '');
|
||||
@@ -212,6 +219,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'RegelungVerzoegert',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RegelnVerzoegert', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'PrognoseSenden',
|
||||
0,
|
||||
@@ -238,13 +250,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
$this->konfiguriereEnergiearchiv();
|
||||
$this->synchronisiereEnergievisualisierungen();
|
||||
$this->konfiguriereEnergiearchiv();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||
$this->SetTimerInterval('Regelzyklus', 0);
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', 0);
|
||||
$this->SetTimerInterval('RegelungVerzoegert', 0);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->SetTimerInterval('StoerungenSenden', 0);
|
||||
$this->SetTimerInterval('MesswerteAufzeichnen', 0);
|
||||
@@ -256,6 +269,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->SetTimerInterval('Regelzyklus', 0);
|
||||
$this->SetTimerInterval('VorgabenErneuern', 0);
|
||||
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
|
||||
$this->SetTimerInterval('RegelungVerzoegert', 0);
|
||||
$this->SetTimerInterval('PrognoseSenden', 0);
|
||||
$this->SetTimerInterval(
|
||||
'StoerungenSenden',
|
||||
@@ -449,6 +463,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->regeln((bool) $wert);
|
||||
return;
|
||||
|
||||
case 'RegelnVerzoegert':
|
||||
$this->SetTimerInterval('RegelungVerzoegert', 0);
|
||||
$this->regeln(false);
|
||||
return;
|
||||
|
||||
case 'PrognoseSenden':
|
||||
$this->sendePrognoseTelemetrie();
|
||||
return;
|
||||
@@ -616,7 +635,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
];
|
||||
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
|
||||
$this->aktualisiereEnergieflusswerte();
|
||||
$this->regeln(false);
|
||||
$this->aktualisiereDiagrammMesswerte();
|
||||
|
||||
// Den synchronen Rueckruf in die noch meldende Verbraucherinstanz vermeiden.
|
||||
$this->SetTimerInterval('RegelungVerzoegert', 100);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $daten */
|
||||
@@ -1008,13 +1030,20 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
|
||||
foreach ($ids as $instanzID) {
|
||||
$eintrag = $cache[(string) $instanzID] ?? null;
|
||||
$grund = '';
|
||||
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) {
|
||||
$grund = 'Noch keine Meldung';
|
||||
} elseif (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
|
||||
$grund = 'Meldung ist veraltet';
|
||||
} elseif (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) {
|
||||
$grund = 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart;
|
||||
$grund = $this->verbraucherMeldegrund($eintrag, $betriebsart, $timeout);
|
||||
$synchronisierungsfehler = '';
|
||||
if (in_array($grund, ['Noch keine Meldung', 'Meldung ist veraltet'], true)) {
|
||||
try {
|
||||
$this->SetBuffer('VerbraucherSynchronisierungAktiv', '1');
|
||||
IPS_RequestAction($instanzID, 'Melden', true);
|
||||
} catch (Throwable $fehler) {
|
||||
$synchronisierungsfehler = $fehler->getMessage();
|
||||
} finally {
|
||||
$this->SetBuffer('VerbraucherSynchronisierungAktiv', '0');
|
||||
}
|
||||
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||
$eintrag = $cache[(string) $instanzID] ?? null;
|
||||
$grund = $this->verbraucherMeldegrund($eintrag, $betriebsart, $timeout);
|
||||
}
|
||||
|
||||
if ($grund !== '') {
|
||||
@@ -1025,8 +1054,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
try {
|
||||
$this->sendeManagerdaten($instanzID, $betriebsart, null);
|
||||
} catch (Throwable $fehler) {
|
||||
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: '
|
||||
. $fehler->getMessage();
|
||||
$synchronisierungsfehler = $fehler->getMessage();
|
||||
}
|
||||
if ($synchronisierungsfehler !== '') {
|
||||
$stoerungen[] = $detail['Name'] . ': Vollmeldung konnte nicht angefordert werden: '
|
||||
. $synchronisierungsfehler;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1039,6 +1071,27 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
return $aktuell;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $eintrag */
|
||||
private function verbraucherMeldegrund(
|
||||
?array $eintrag,
|
||||
string $betriebsart,
|
||||
int $timeout
|
||||
): string {
|
||||
if (!isset($eintrag['EmpfangenAm'], $eintrag['Daten'])
|
||||
|| !is_array($eintrag['Daten'])
|
||||
) {
|
||||
return 'Noch keine Meldung';
|
||||
}
|
||||
if (time() - (int) $eintrag['EmpfangenAm'] > $timeout) {
|
||||
return 'Meldung ist veraltet';
|
||||
}
|
||||
if (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) {
|
||||
return 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function setzeNichtGeregelteVerbraucher(string $grund): void
|
||||
{
|
||||
try {
|
||||
@@ -2120,6 +2173,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
|
||||
}
|
||||
|
||||
$this->aktualisiereDiagrammMesswerte(true);
|
||||
$funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID);
|
||||
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
|
||||
$this->aktualisiereFunFacts($funFactsID);
|
||||
@@ -2647,6 +2701,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
||||
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
|
||||
}
|
||||
}
|
||||
foreach (['DiagrammPVModus', 'DiagrammBatterieModus'] as $property) {
|
||||
if (!in_array($this->ReadPropertyInteger($property), [0, 1, 2], true)) {
|
||||
throw new InvalidArgumentException($property . ' muss 0, 1 oder 2 sein.');
|
||||
}
|
||||
}
|
||||
if (!in_array($this->ReadPropertyInteger('EnergieflussLeistungseinheit'), [0, 1], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
'EnergieflussLeistungseinheit muss 0 oder 1 sein.'
|
||||
);
|
||||
}
|
||||
$energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
|
||||
$energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
|
||||
if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) {
|
||||
|
||||
@@ -53,6 +53,8 @@ EinstellungenInVisu und LoggingEin stammen aus VerbraucherBasisTrait.
|
||||
| ReserveLadezustand | Float | 20 | Peakshaving-Reserve in Prozent |
|
||||
| MindestLadezustand | Float | 10 | Untere Entladegrenze in Prozent |
|
||||
| LadezustandHysterese | Float | 2 | Hysterese oberhalb der Reserve |
|
||||
| NachladenMitMaximalleistung | Boolean | true | Verwendet fuer schutzbedingtes Nachladen die dynamische maximale Ladeleistung |
|
||||
| MaximaleNachladeleistung | Integer | 0 | Benutzerdefinierte Nachladegrenze in W; bei deaktivierter Maximalleistung muss der Wert groesser als 0 sein |
|
||||
| MesswertMaxAlter | Integer | 60 | Maximales Messwertalter in Sekunden |
|
||||
| Aenderungssperre | Integer | 4 | Sperrzeit nach Sollwertaenderung |
|
||||
| DiagnosevariablenAnzeigen | Boolean | false | Technische Variablen einblenden |
|
||||
@@ -81,9 +83,12 @@ wird.
|
||||
|
||||
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
|
||||
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
|
||||
gemeldet. Die Hysterese wird als persistenter Modulzustand gefuehrt. Bei
|
||||
Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen Messwerten
|
||||
lautet das Angebot [0].
|
||||
gemeldet. Bei schutzbedingtem Nachladen innerhalb oder unterhalb der Reserve
|
||||
begrenzt MaximaleNachladeleistung die positive Angebotsgrenze, wenn
|
||||
NachladenMitMaximalleistung deaktiviert ist. Der normale Ladebereich oberhalb
|
||||
der Reserve bleibt unveraendert. Die Hysterese wird als persistenter Modulzustand
|
||||
gefuehrt. Bei Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen
|
||||
Messwerten lautet das Angebot [0].
|
||||
|
||||
## Variablen
|
||||
|
||||
|
||||
@@ -50,22 +50,22 @@ als ungueltig behandelt und ergeben bis zur gueltigen 1-/3-Phasenmeldung nur
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet im Peakbetrieb. |
|
||||
| `Meldeintervall` | Integer | `10 s` | Periodische Vollmeldung an den Manager. |
|
||||
| `VorgabeTimeout` | Integer | `120 s` | Ablauf einer nicht erneuerten Manager-Vorgabe. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Ladefreigabe und Solarladen. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Ladefreigabe, Solarladen, Istleistung und Sollleistung. |
|
||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Zeigt technische Diagnosewerte. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert Debugmeldungen ohne Geheimnisse. |
|
||||
|
||||
## Variablen
|
||||
|
||||
Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`,
|
||||
`Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte
|
||||
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` wartet auf Start,
|
||||
`3` laedt, `4` geladen oder `5` Fehler.
|
||||
`Ladestrom` und `Phasenzahl`. Mit `EinstellungenInVisu=true` erscheinen
|
||||
zusaetzlich `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung`.
|
||||
|
||||
Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie
|
||||
Mit `DiagnosevariablenAnzeigen=true` werden der numerische `Fahrzeugstatus`
|
||||
und die gemeinsamen EMS-Diagnosewerte sowie
|
||||
`GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
|
||||
`AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
|
||||
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
|
||||
`LeistungsangebotDiagnose`. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
|
||||
`LeistungsangebotDiagnose` angezeigt. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
|
||||
die Autorisierung fehlt.
|
||||
|
||||
## Regelung
|
||||
@@ -83,6 +83,11 @@ Ladestation Stand-Alone:
|
||||
Die niedrigste Grenze aus Property, Observation `47` und Observation `104`
|
||||
bestimmt den angebotenen Maximalstrom. Eine neue Observation berechnet das
|
||||
Angebot sofort neu und meldet die Aenderung kurz gebuendelt an den Manager.
|
||||
Beim Aktivieren oder Umschalten von Solarladen bleibt der bisherige
|
||||
Geraetestrom bis zur neuen Manager-Vorgabe unveraendert. Ohne Managerantwort
|
||||
greift nach zwei Sekunden das lokale Ersatzverhalten. Sicherheitsabschaltungen
|
||||
werden nicht verzoegert.
|
||||
|
||||
Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort
|
||||
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
|
||||
frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0`
|
||||
|
||||
@@ -43,11 +43,11 @@ Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
|
||||
3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
|
||||
Strom fuehren, wird dreiphasiges Laden direkt erkannt.
|
||||
4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
|
||||
Modul zunaechst den konfigurierten Maximalstrom als Erkennungsstrom vor. Bis
|
||||
zur abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot
|
||||
`[0]`.
|
||||
5. Nach `Phasenerkennungszeit` gilt wie in Enelix 1: ueber 7500 W werden drei
|
||||
Phasen erkannt, andernfalls eine Phase. Die erkannte Phasenzahl bleibt bis
|
||||
Modul zunaechst nur den Mindeststrom von 6 A als Erkennungsstrom vor. Bis zur
|
||||
abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot `[0]`.
|
||||
5. Nach `Phasenerkennungszeit` wird die gemessene Leistung relativ zum
|
||||
angeforderten Erkennungsstrom ausgewertet. Dadurch lassen sich eine und drei
|
||||
Phasen bereits bei 6 A unterscheiden. Die erkannte Phasenzahl bleibt bis
|
||||
zum Ausstecken erhalten und kippt waehrend einer Ladepause nicht zurueck.
|
||||
6. Der Ladestrom wird aus der Leistung mit `230 W/A` einphasig beziehungsweise
|
||||
`684 W/A` dreiphasig berechnet.
|
||||
@@ -84,6 +84,12 @@ Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
|
||||
bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
|
||||
die maximale angebotene Leistung.
|
||||
|
||||
Beim Aktivieren oder Umschalten von Solarladen bleibt ein bereits gesetzter
|
||||
Geraetestrom zunaechst unveraendert. Das neue Angebot wird zuerst an den
|
||||
zugeordneten Manager gemeldet; dessen neue Vorgabe wird anschliessend direkt
|
||||
uebernommen. Antwortet kein Manager, greift nach zwei Sekunden das lokale
|
||||
Ersatzverhalten. Sicherheitsabschaltungen wirken weiterhin sofort.
|
||||
|
||||
Nach einer Leistungsaenderung wird die aktuelle Stufe fuer
|
||||
`ZeitZwischenZustandswechseln` gehalten. Waehrend der
|
||||
`Mindesteinschaltdauer` bleiben positive Leistungsstufen regelbar, `0 W`
|
||||
@@ -115,7 +121,7 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
||||
| `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. |
|
||||
| `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. |
|
||||
| `VorgabeTimeout` | Integer | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe ohne Erneuerung. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe` und `Solarladen` als bedienbare Variablen ein. |
|
||||
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung` ein. |
|
||||
| `LoggingEin` | Boolean | `false` | Aktiviert das laufende Debug-Protokoll des Moduls. |
|
||||
|
||||
### Ladestation und Diagnose
|
||||
@@ -129,7 +135,7 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
||||
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
||||
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
||||
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
||||
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der Maximalstrom-Probe vor der leistungsbasierten Phasenerkennung. |
|
||||
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der 6-A-Probe vor der leistungsbasierten Phasenerkennung. |
|
||||
| `FahrzeugstromErkennungszeit` | Integer | `90 s` | Zeit, die eine stabile Stromunterschreitung anliegen muss, bevor die Fahrzeuggrenze mit 2,5 A Reserve uebernommen wird. |
|
||||
| `ZeitZwischenZustandswechseln` | Integer | `1 min` | Mindestabstand zwischen zwei Leistungsaenderungen. |
|
||||
| `Mindesteinschaltdauer` | Integer | `0 min` | Mindestdauer nach dem Einschalten, in der kein Abschalten auf 0 W angeboten wird. |
|
||||
@@ -152,22 +158,22 @@ Immer sichtbar:
|
||||
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. |
|
||||
| `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. |
|
||||
| `FahrzeugGeladen` | Boolean / Anzeige | Ergebnis der kompatiblen Enelix-1-Voll-Erkennung. |
|
||||
| `Fahrzeugstatus` | Integer / Anzeige | Normalisierter Status von `0` bis `4`. |
|
||||
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Ladestrom in A. |
|
||||
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig oder `3` dreiphasig. |
|
||||
|
||||
Nur mit `EinstellungenInVisu=true` sichtbar und bedienbar:
|
||||
Nur mit `EinstellungenInVisu=true` sichtbar; Ladefreigabe und Solarladen sind bedienbar:
|
||||
|
||||
- `Ladefreigabe`
|
||||
- `Solarladen`
|
||||
- `Istleistung`
|
||||
- `Sollleistung`
|
||||
|
||||
Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
|
||||
|
||||
| Ident | Beschreibung |
|
||||
| --- | --- |
|
||||
| `Istleistung` | Von der Ladestation gemessene Leistung in W. |
|
||||
| `Leistungsquelle` | Immer `2` fuer eine gemessene Leistung. |
|
||||
| `Sollleistung` | Letzte angenommene Manager-Vorgabe in W. |
|
||||
| `Fahrzeugstatus` | Normalisierter Status von `0` bis `4`. |
|
||||
| `SollwertGueltig` | Zeigt, ob die Vorgabe noch gueltig und im aktuellen Angebot enthalten ist. |
|
||||
| `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. |
|
||||
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
|
||||
@@ -253,8 +259,8 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
|
||||
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
||||
Leistungsangebot `[0]`.
|
||||
- Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
|
||||
zunaechst der konfigurierte Maximalstrom zur Erkennung angefordert.
|
||||
- Nach der Erkennungszeit werden bis 7500 W eine Phase, darueber drei Phasen
|
||||
zunaechst der Mindeststrom von 6 A zur Erkennung angefordert.
|
||||
- Nach der Erkennungszeit werden eine oder drei Phasen relativ zum Pruefstrom
|
||||
gespeichert. Bei go-e koennen die einzelnen Phasenstroeme die Erkennung
|
||||
bereits vorher abschliessen.
|
||||
- Die erkannte Phasenzahl bleibt bei einer anschliessenden Ladepause stabil.
|
||||
@@ -298,7 +304,7 @@ URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
|
||||
geprueft.
|
||||
|
||||
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, die Maximalstrom-Probe
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, die Mindeststrom-Probe
|
||||
beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
|
||||
Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
|
||||
Umschaltung ueber den `Solarladen`-Button.
|
||||
|
||||
@@ -310,15 +310,36 @@ Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
|
||||
wird mit genau `11000 W` zugeteilt.
|
||||
|
||||
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
|
||||
sortiert. Bei gleicher Priorität bevorzugt der Manager wie Enelix 1 den
|
||||
Verbraucher mit der bisher kleineren bezogenen Energie; verglichen wird in
|
||||
2-kWh-Gruppen. Erst wenn auch diese Gruppe gleich ist, entscheidet stabil die
|
||||
Instanz-ID. Zuerst reserviert der Manager für jeden Verbraucher den kleinsten
|
||||
erlaubten Leistungswert. Nur das danach
|
||||
verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und
|
||||
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch
|
||||
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im
|
||||
`Verbraucherstatus` dokumentiert.
|
||||
sortiert. Zuerst reserviert der Manager die richtungsneutralen erlaubten Werte
|
||||
sowie die gemessene Leistung fester oder gesperrter Verbraucher.
|
||||
|
||||
Positives Restbudget wird innerhalb jeder Priorität schrittweise verteilt:
|
||||
Zuerst kommt die kleinste nächste erreichbare **absolute Sollleistung**, nicht
|
||||
die kleinste Erhöhung. Bei gleicher nächster Sollleistung entscheidet die exakt
|
||||
gezählte bezogene Energie in Wh, danach stabil die Instanz-ID. Die bisherigen
|
||||
2-kWh-Gruppen werden für diese Vergabe nicht mehr verwendet. Vom Budget wird nur
|
||||
die Differenz zum bereits zugeteilten Sollwert abgezogen. Ein nicht finanzierbarer
|
||||
Schritt wird übersprungen; andere passende Schritte und danach tiefere
|
||||
Prioritäten können das verbleibende Budget nutzen.
|
||||
|
||||
Beispiel bei gleicher Priorität: A bietet `[0,100,200,400,800]` bei 10 kWh,
|
||||
B bietet `[0,100,110,500,780,1500]` bei 12 kWh. Die Zuteilungsfolge bei
|
||||
ausreichendem Budget ist `A100, B100, B110, A200, A400, B500, B780, A800, B1500`.
|
||||
Die zugehörigen Gesamtbudgets sind 100, 200, 210, 310, 510, 900, 1180, 1580 und
|
||||
2300 W. Energie entscheidet nur bei gleichen nächsten Leistungsstufen, nicht
|
||||
mehr über die vollständige Versorgung eines Verbrauchers vor allen anderen.
|
||||
|
||||
Ganzzahlige Bereiche folgen derselben Reihenfolge wie einzelne Wattstufen,
|
||||
werden aber über gemeinsame Leistungsniveaus effizient verarbeitet. Lücken
|
||||
werden nie durch unzulässige Werte geschlossen. Bei negativem Restbudget bleibt
|
||||
die bestehende Defizitregelung einschliesslich ihrer Energiegruppen unverändert.
|
||||
Eine verbleibende Abweichung wird im `Verbraucherstatus` dokumentiert.
|
||||
|
||||
Für das Update sind keine neuen Properties oder eine Migration nötig. Bestehende
|
||||
Energiezähler, Sperrzeiten und Freigaben bleiben erhalten. Nach dem Modulupdate
|
||||
gilt die neue Vergabe beim nächsten Regellauf. Vor einem Update einer laufenden
|
||||
Anlage Einstellungen sichern und die neuen Sollwerte kontrolliert prüfen;
|
||||
die Git-Veröffentlichung allein aktualisiert keine laufende Installation.
|
||||
|
||||
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
|
||||
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
|
||||
|
||||
+10
-5
@@ -52,7 +52,8 @@ final class BatterieRegler
|
||||
float $netzleistungW,
|
||||
float $istleistungW,
|
||||
float $maxLadenW,
|
||||
float $maxEntladenW
|
||||
float $maxEntladenW,
|
||||
?float $maxNachladenW = null
|
||||
): array {
|
||||
self::pruefeLadezustandsgrenzen($reserve, 0.0);
|
||||
if ($minimum < 0.0 || $minimum > 100.0 || $minimum > $reserve) {
|
||||
@@ -61,12 +62,16 @@ final class BatterieRegler
|
||||
);
|
||||
}
|
||||
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
|
||||
if ($maxNachladenW !== null && $maxNachladenW < 0.0) {
|
||||
throw new InvalidArgumentException('Maximale Nachladeleistung darf nicht negativ sein.');
|
||||
}
|
||||
|
||||
if (!$aktiv || !$enelixSteuert) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$maxLaden = (int) floor($maxLadenW);
|
||||
$maxNachladen = (int) floor(min($maxLadenW, $maxNachladenW ?? $maxLadenW));
|
||||
$maxEntladen = (int) floor($maxEntladenW);
|
||||
if ($peakbetrieb) {
|
||||
if ($ladezustand > $reserve && !$hystereseAktiv) {
|
||||
@@ -80,10 +85,10 @@ final class BatterieRegler
|
||||
return self::leistungsbereich(-$maxEntladen, 0);
|
||||
}
|
||||
if ($ladezustand > $minimum) {
|
||||
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||
return self::leistungsbereich(-$maxEntladen, $maxNachladen);
|
||||
}
|
||||
|
||||
return self::leistungsbereich(0, $maxLaden);
|
||||
return self::leistungsbereich(0, $maxNachladen);
|
||||
}
|
||||
|
||||
if ($ladezustand > 99.0) {
|
||||
@@ -93,10 +98,10 @@ final class BatterieRegler
|
||||
return self::leistungsbereich(-$maxEntladen, $maxLaden);
|
||||
}
|
||||
if ($ladezustand >= $reserve && $hystereseAktiv) {
|
||||
return self::leistungsbereich(0, $maxLaden);
|
||||
return self::leistungsbereich(0, $maxNachladen);
|
||||
}
|
||||
|
||||
return [$maxLaden];
|
||||
return [$maxNachladen];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,7 +91,7 @@ final class LadestationAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int}
|
||||
* @return array{Rohstatus: int, Istleistung_W: float, Sollstrom_A?: int, Maximalstrom_A?: int, Iststrom_A?: float, Phasenzahl?: int}
|
||||
*/
|
||||
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
||||
{
|
||||
@@ -115,6 +115,20 @@ final class LadestationAdapter
|
||||
'Rohstatus' => (int) $daten['car'],
|
||||
'Istleistung_W' => (float) $daten['nrg'][11] * $leistungsfaktor,
|
||||
];
|
||||
if (isset($daten['amp']) && is_numeric($daten['amp'])) {
|
||||
$sollstrom = (int) $daten['amp'];
|
||||
$freigabeEntzogen = isset($daten['alw']) && $daten['alw'] === false;
|
||||
$erzwungenAus = $geraetetyp === self::GO_E_GEMINI
|
||||
&& isset($daten['frc'])
|
||||
&& (int) $daten['frc'] === 1;
|
||||
$status['Sollstrom_A'] = $freigabeEntzogen || $erzwungenAus ? 0 : $sollstrom;
|
||||
}
|
||||
if (isset($daten['ama']) && is_numeric($daten['ama'])) {
|
||||
$maximalstrom = (int) $daten['ama'];
|
||||
if ($maximalstrom >= 6 && $maximalstrom <= 32) {
|
||||
$status['Maximalstrom_A'] = $maximalstrom;
|
||||
}
|
||||
}
|
||||
if (isset($daten['nrg'][4], $daten['nrg'][5], $daten['nrg'][6])
|
||||
&& is_numeric($daten['nrg'][4])
|
||||
&& is_numeric($daten['nrg'][5])
|
||||
|
||||
@@ -15,6 +15,22 @@ final class LadestationRegler
|
||||
private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
|
||||
private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
|
||||
|
||||
public static function erkennePhasenzahlAusLeistung(
|
||||
float $leistung,
|
||||
int $angeforderterStrom
|
||||
): int {
|
||||
if (!is_finite($leistung) || $leistung < 0.0) {
|
||||
throw new InvalidArgumentException('Istleistung muss endlich und darf nicht negativ sein.');
|
||||
}
|
||||
if ($angeforderterStrom < 6 || $angeforderterStrom > 32) {
|
||||
throw new InvalidArgumentException('Erkennungsstrom muss zwischen 6 und 32 A liegen.');
|
||||
}
|
||||
|
||||
$trennleistung = 2 * self::LEISTUNG_PRO_AMPERE_EINPHASIG * $angeforderterStrom;
|
||||
|
||||
return $leistung > $trennleistung ? self::PHASEN_DREI : self::PHASEN_EIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
|
||||
* Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
|
||||
|
||||
+611
-62
@@ -55,6 +55,38 @@ trait ManagerEnergieTrait
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IPS_GetChildrenIDs($this->InstanceID) as $variableID) {
|
||||
$objekt = IPS_GetObject($variableID);
|
||||
$ident = (string) ($objekt['ObjectIdent'] ?? '');
|
||||
if ((int) ($objekt['ObjectType'] ?? -1) !== 2
|
||||
|| !str_starts_with($ident, 'Diagramm')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$istEnergie = str_contains($ident, 'Energie')
|
||||
|| str_contains($ident, 'BatterieLaden_')
|
||||
|| str_contains($ident, 'BatterieEntladen_');
|
||||
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
|
||||
AC_SetAggregationType($archivID, $variableID, $istEnergie ? 1 : 0);
|
||||
AC_SetGraphStatus($archivID, $variableID, $aktiv);
|
||||
$regeln = [-1 => $istEnergie ? $energietyp : $leistungstyp];
|
||||
$verdichten = $istEnergie
|
||||
? $this->ReadPropertyInteger('EnergieVerdichtenMonate')
|
||||
: 0;
|
||||
if ($verdichten > 0) {
|
||||
$regeln[$verdichten] = 3;
|
||||
}
|
||||
$loeschen = $istEnergie
|
||||
? $this->ReadPropertyInteger('EnergieLoeschenMonate')
|
||||
: $this->ReadPropertyInteger('LeistungLoeschenMonate');
|
||||
if ($loeschen > 0) {
|
||||
$regeln[$loeschen] = 7;
|
||||
}
|
||||
if ($aktiv && $this->setzeVerdichtung($archivID, $variableID, $regeln)) {
|
||||
$geaendert[] = $variableID;
|
||||
}
|
||||
}
|
||||
|
||||
IPS_ApplyChanges($archivID);
|
||||
foreach (array_unique($geaendert) as $variableID) {
|
||||
AC_ReAggregateVariable($archivID, $variableID);
|
||||
@@ -129,6 +161,7 @@ trait ManagerEnergieTrait
|
||||
}
|
||||
$this->entferneDiagrammUnterobjekt('Leistungsdiagramm');
|
||||
$this->entferneDiagrammUnterobjekt('Energiediagramm');
|
||||
$this->WriteAttributeString('EnergiediagrammKonfiguration', '{}');
|
||||
}
|
||||
|
||||
private function synchronisiereFunFacts(bool $anzeigen): void
|
||||
@@ -154,17 +187,18 @@ trait ManagerEnergieTrait
|
||||
return;
|
||||
}
|
||||
$instanzID = $this->findeUnterobjekt('Energiefluss');
|
||||
if ($instanzID === 0) {
|
||||
return;
|
||||
if ($instanzID !== 0) {
|
||||
$objekt = IPS_GetObject($instanzID);
|
||||
$modulID = $objekt['ObjectType'] === 1
|
||||
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||
: '';
|
||||
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
|
||||
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
|
||||
}
|
||||
IPS_SetHidden($instanzID, true);
|
||||
}
|
||||
$objekt = IPS_GetObject($instanzID);
|
||||
$modulID = $objekt['ObjectType'] === 1
|
||||
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||
: '';
|
||||
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
|
||||
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
|
||||
}
|
||||
IPS_SetHidden($instanzID, true);
|
||||
$this->WriteAttributeString('EnergieflussKonfiguration', '{}');
|
||||
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', true);
|
||||
}
|
||||
|
||||
private function entferneInstanzUnterobjekt(string $ident, string $modulID): void
|
||||
@@ -216,6 +250,9 @@ trait ManagerEnergieTrait
|
||||
if ($modulID !== self::ENERGY_PIE_MODULE_ID) {
|
||||
throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.');
|
||||
}
|
||||
IPS_SetHidden($instanzID, false);
|
||||
|
||||
return 'Bestehende Energy-Pie-Konfiguration bleibt unveraendert.';
|
||||
}
|
||||
IPS_SetName($instanzID, 'Energy Pie');
|
||||
IPS_SetPosition($instanzID, 500);
|
||||
@@ -239,6 +276,7 @@ trait ManagerEnergieTrait
|
||||
if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) {
|
||||
throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.');
|
||||
}
|
||||
$neuErzeugen = $this->ReadAttributeBoolean('EnergieflussNeuErzeugen');
|
||||
$instanzID = $this->findeUnterobjekt('Energiefluss');
|
||||
if ($instanzID === 0) {
|
||||
$instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID);
|
||||
@@ -252,7 +290,15 @@ trait ManagerEnergieTrait
|
||||
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
|
||||
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
|
||||
}
|
||||
if (!$neuErzeugen) {
|
||||
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
|
||||
IPS_SetHidden($instanzID, false);
|
||||
$this->aktualisiereEnergieflusswerte();
|
||||
|
||||
return 'Bestehende Energiefluss-Konfiguration bleibt unveraendert.';
|
||||
}
|
||||
}
|
||||
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
|
||||
IPS_SetName($instanzID, 'Energiefluss');
|
||||
IPS_SetPosition($instanzID, 540);
|
||||
IPS_SetHidden($instanzID, false);
|
||||
@@ -289,7 +335,6 @@ trait ManagerEnergieTrait
|
||||
$this->energieflussKnoten($netzID, 'Stromnetz', 'grid'),
|
||||
$this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'),
|
||||
];
|
||||
$hatBatterieverbraucher = false;
|
||||
$position = 640;
|
||||
foreach ($verbraucher as $eintrag) {
|
||||
$istID = $this->energieflussLeistungsvariable(
|
||||
@@ -311,17 +356,13 @@ trait ManagerEnergieTrait
|
||||
true,
|
||||
$sollID
|
||||
);
|
||||
$hatBatterieverbraucher = $hatBatterieverbraucher
|
||||
|| $eintrag['Typ'] === 'storage';
|
||||
$position += 10;
|
||||
}
|
||||
if (!$hatBatterieverbraucher) {
|
||||
$knoten[] = $this->energieflussKnoten(
|
||||
$batterieID,
|
||||
'Batteriespeicher',
|
||||
'storage'
|
||||
);
|
||||
}
|
||||
$knoten[] = $this->energieflussKnoten(
|
||||
$batterieID,
|
||||
'Batteriespeicher',
|
||||
'storage'
|
||||
);
|
||||
|
||||
IPS_SetProperty($instanzID, 'Calculation', true);
|
||||
IPS_SetProperty(
|
||||
@@ -332,6 +373,7 @@ trait ManagerEnergieTrait
|
||||
IPS_SetProperty($instanzID, 'EnableLimits', false);
|
||||
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
||||
IPS_ApplyChanges($instanzID);
|
||||
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
|
||||
$this->aktualisiereEnergieflusswerte($verbraucher);
|
||||
|
||||
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
||||
@@ -360,7 +402,10 @@ trait ManagerEnergieTrait
|
||||
|
||||
private function energieflussLeistungsprofil(): string
|
||||
{
|
||||
if ($this->ReadPropertyInteger('EnergieflussLeistungseinheit') !== 1) {
|
||||
$konfiguration = $this->visualisierungsKonfiguration(
|
||||
'EnergieflussKonfiguration'
|
||||
);
|
||||
if ($konfiguration['EnergieflussLeistungseinheit'] !== 1) {
|
||||
return '~Power';
|
||||
}
|
||||
|
||||
@@ -388,18 +433,55 @@ trait ManagerEnergieTrait
|
||||
return $variableID;
|
||||
}
|
||||
|
||||
/** @return array<string, bool|int> */
|
||||
private function visualisierungsKonfiguration(string $attribut): array
|
||||
{
|
||||
$konfiguration = $this->leseJsonAttribut($attribut);
|
||||
$gueltig = isset(
|
||||
$konfiguration['DiagrammPVModus'],
|
||||
$konfiguration['DiagrammBatterieModus'],
|
||||
$konfiguration['LadestationenSeparatAnzeigen'],
|
||||
$konfiguration['VerbraucherSeparatAnzeigen'],
|
||||
$konfiguration['EnergieflussLeistungseinheit']
|
||||
);
|
||||
if ($gueltig) {
|
||||
return $konfiguration;
|
||||
}
|
||||
|
||||
$konfiguration = [
|
||||
'DiagrammPVModus' => $this->ReadPropertyInteger('DiagrammPVModus'),
|
||||
'DiagrammBatterieModus' => $this->ReadPropertyInteger('DiagrammBatterieModus'),
|
||||
'LadestationenSeparatAnzeigen' =>
|
||||
$this->ReadPropertyBoolean('LadestationenSeparatAnzeigen'),
|
||||
'VerbraucherSeparatAnzeigen' =>
|
||||
$this->ReadPropertyBoolean('VerbraucherSeparatAnzeigen'),
|
||||
'EnergieflussLeistungseinheit' =>
|
||||
$this->ReadPropertyInteger('EnergieflussLeistungseinheit'),
|
||||
];
|
||||
$this->WriteAttributeString(
|
||||
$attribut,
|
||||
json_encode($konfiguration, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
return $konfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{
|
||||
* InstanzID: int,
|
||||
* Name: string,
|
||||
* Typ: string,
|
||||
* Istleistung_W: float,
|
||||
* Sollleistung_W: float
|
||||
* Sollleistung_W: float,
|
||||
* Energie_Wh: float
|
||||
* }>
|
||||
*/
|
||||
private function energieflussVerbraucher(): array
|
||||
{
|
||||
private function energieflussVerbraucher(
|
||||
string $konfigurationsAttribut = 'EnergieflussKonfiguration'
|
||||
): array {
|
||||
$konfiguration = $this->visualisierungsKonfiguration($konfigurationsAttribut);
|
||||
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||
$energien = $this->leseJsonAttribut('VerbraucherEnergie');
|
||||
$ergebnis = [];
|
||||
foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) {
|
||||
if (!IPS_InstanceExists($instanzID)) {
|
||||
@@ -416,17 +498,30 @@ trait ManagerEnergieTrait
|
||||
(string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||
);
|
||||
$katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? '';
|
||||
if ($katalog === 'battery') {
|
||||
continue;
|
||||
}
|
||||
if ($katalog === 'ev_charger'
|
||||
&& !$konfiguration['LadestationenSeparatAnzeigen']
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if ($katalog !== 'ev_charger'
|
||||
&& !$konfiguration['VerbraucherSeparatAnzeigen']
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$verbraucherEnergie = $energien[(string) $instanzID]['Energie_Wh'] ?? 0.0;
|
||||
|
||||
$ergebnis[] = [
|
||||
'InstanzID' => $instanzID,
|
||||
'Name' => IPS_GetName($instanzID),
|
||||
'Typ' => match ($katalog) {
|
||||
'battery' => 'storage',
|
||||
'ev_charger' => 'wallbox',
|
||||
default => 'consumer',
|
||||
},
|
||||
'Typ' => $katalog === 'ev_charger' ? 'wallbox' : 'consumer',
|
||||
'Istleistung_W' => $istleistung,
|
||||
'Sollleistung_W' => $sollleistung,
|
||||
'Energie_Wh' => is_numeric($verbraucherEnergie)
|
||||
? (float) $verbraucherEnergie
|
||||
: 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -443,7 +538,6 @@ trait ManagerEnergieTrait
|
||||
}
|
||||
$verbraucher = $verbraucher ?? $this->energieflussVerbraucher();
|
||||
$verbraucherleistung = 0.0;
|
||||
$hatBatterieverbraucher = false;
|
||||
foreach ($verbraucher as $eintrag) {
|
||||
$this->setzeEnergieflussLeistungswert(
|
||||
'EnergieflussIst_' . $eintrag['InstanzID'],
|
||||
@@ -453,11 +547,7 @@ trait ManagerEnergieTrait
|
||||
'EnergieflussSoll_' . $eintrag['InstanzID'],
|
||||
(float) $eintrag['Sollleistung_W']
|
||||
);
|
||||
if ($eintrag['Typ'] === 'storage') {
|
||||
$hatBatterieverbraucher = true;
|
||||
} else {
|
||||
$verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']);
|
||||
}
|
||||
$verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -472,12 +562,10 @@ trait ManagerEnergieTrait
|
||||
'EnergieflussHaus',
|
||||
max(0.0, $leistungen['Haus'] - $verbraucherleistung)
|
||||
);
|
||||
if (!$hatBatterieverbraucher) {
|
||||
$this->setzeEnergieflussLeistungswert(
|
||||
'EnergieflussBatterie',
|
||||
$leistungen['Batterie']
|
||||
);
|
||||
}
|
||||
$this->setzeEnergieflussLeistungswert(
|
||||
'EnergieflussBatterie',
|
||||
$leistungen['Batterie']
|
||||
);
|
||||
}
|
||||
|
||||
private function setzeEnergieflussLeistungswert(string $ident, float $watt): void
|
||||
@@ -486,33 +574,487 @@ trait ManagerEnergieTrait
|
||||
if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) {
|
||||
return;
|
||||
}
|
||||
$faktor = $this->ReadPropertyInteger('EnergieflussLeistungseinheit') === 1
|
||||
? 0.001
|
||||
: 1.0;
|
||||
$konfiguration = $this->visualisierungsKonfiguration(
|
||||
'EnergieflussKonfiguration'
|
||||
);
|
||||
$faktor = $konfiguration['EnergieflussLeistungseinheit'] === 1 ? 0.001 : 1.0;
|
||||
SetValue($variableID, $watt * $faktor);
|
||||
}
|
||||
|
||||
private function erstelleEnergiediagramme(): string
|
||||
{
|
||||
$leistung = [
|
||||
$this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'),
|
||||
$this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'),
|
||||
$this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'),
|
||||
$this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'),
|
||||
];
|
||||
$energie = [
|
||||
$this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'),
|
||||
$this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'),
|
||||
$this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'),
|
||||
$this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'),
|
||||
$this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'),
|
||||
$this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'),
|
||||
];
|
||||
$konfiguration = $this->visualisierungsKonfiguration(
|
||||
'EnergiediagrammKonfiguration'
|
||||
);
|
||||
$pvAnlagen = $this->diagrammPVAnlagen();
|
||||
$batterien = $this->diagrammBatterien();
|
||||
$verbraucher = $this->energieflussVerbraucher(
|
||||
'EnergiediagrammKonfiguration'
|
||||
);
|
||||
$this->registriereDiagrammVariablen($pvAnlagen, $batterien, $verbraucher);
|
||||
$this->aktualisiereDiagrammMesswerte(true);
|
||||
|
||||
$leistung = [];
|
||||
$energie = [];
|
||||
if ($konfiguration['DiagrammPVModus'] !== 1) {
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
'PVLeistungArchiv',
|
||||
'PV gesamt',
|
||||
'#F4C542',
|
||||
'~Power'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'PVEnergie',
|
||||
'PV gesamt',
|
||||
'#F4C542',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
if ($konfiguration['DiagrammPVModus'] !== 0) {
|
||||
foreach ($pvAnlagen as $index => $anlage) {
|
||||
$farbe = $this->diagrammFarbe($index);
|
||||
if ($anlage['LeistungVariableID'] > 0) {
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
$anlage['LeistungIdent'],
|
||||
$anlage['Name'],
|
||||
$farbe,
|
||||
'~Power'
|
||||
);
|
||||
}
|
||||
if ($anlage['EnergieVariableID'] > 0) {
|
||||
$energie[] = $this->chartDatensatz(
|
||||
$anlage['EnergieIdent'],
|
||||
$anlage['Name'],
|
||||
$farbe,
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hausTitel = $verbraucher === [] ? 'Haus' : 'Haus ohne separat dargestellte Verbraucher';
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
'DiagrammHausLeistung',
|
||||
$hausTitel,
|
||||
'#22A06B',
|
||||
'~Power'
|
||||
);
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
'NetzleistungArchiv',
|
||||
'Netz',
|
||||
'#E95D5D',
|
||||
'~Power'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'DiagrammHausEnergie',
|
||||
$hausTitel,
|
||||
'#22A06B',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'NetzbezugEnergie',
|
||||
'Netzbezug',
|
||||
'#E95D5D',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'EinspeisungEnergie',
|
||||
'Netzeinspeisung',
|
||||
'#2F80ED',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
|
||||
if ($konfiguration['DiagrammBatterieModus'] !== 1) {
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
'BatterieleistungArchiv',
|
||||
'Batterie gesamt',
|
||||
'#8B5CF6',
|
||||
'~Power'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'BatterieLadenEnergie',
|
||||
'Batterie laden gesamt',
|
||||
'#8B5CF6',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'BatterieEntladenEnergie',
|
||||
'Batterie entladen gesamt',
|
||||
'#F59E0B',
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
if ($konfiguration['DiagrammBatterieModus'] !== 0) {
|
||||
foreach ($batterien as $index => $batterie) {
|
||||
$farbe = $this->diagrammFarbe($index + count($pvAnlagen));
|
||||
if ($batterie['LeistungVariableID'] > 0) {
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
$batterie['LeistungIdent'],
|
||||
$batterie['Name'],
|
||||
$farbe,
|
||||
'~Power'
|
||||
);
|
||||
}
|
||||
if ($batterie['LadeenergieVariableID'] > 0) {
|
||||
$energie[] = $this->chartDatensatz(
|
||||
$batterie['LadeenergieIdent'],
|
||||
$batterie['Name'] . ' laden',
|
||||
$farbe,
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
if ($batterie['EntladeenergieVariableID'] > 0) {
|
||||
$energie[] = $this->chartDatensatz(
|
||||
$batterie['EntladeenergieIdent'],
|
||||
$batterie['Name'] . ' entladen',
|
||||
$farbe,
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($verbraucher as $index => $eintrag) {
|
||||
$farbe = $this->diagrammFarbe(
|
||||
$index + count($pvAnlagen) + count($batterien)
|
||||
);
|
||||
$leistung[] = $this->chartDatensatz(
|
||||
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
|
||||
$eintrag['Name'],
|
||||
$farbe,
|
||||
'~Power'
|
||||
);
|
||||
$energie[] = $this->chartDatensatz(
|
||||
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
|
||||
$eintrag['Name'],
|
||||
$farbe,
|
||||
'~Electricity',
|
||||
'bar'
|
||||
);
|
||||
}
|
||||
|
||||
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
|
||||
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
|
||||
|
||||
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.';
|
||||
}
|
||||
/** @return list<array<string, mixed>> */
|
||||
private function diagrammPVAnlagen(): array
|
||||
{
|
||||
$verwendet = [];
|
||||
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenPVFlaechen') as $flaeche) {
|
||||
$wechselrichterID = trim((string) ($flaeche['WechselrichterID'] ?? ''));
|
||||
if ($wechselrichterID !== '') {
|
||||
$verwendet[$wechselrichterID] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$ergebnis = [];
|
||||
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenWechselrichter') as $eintrag) {
|
||||
$id = trim((string) ($eintrag['ID'] ?? ''));
|
||||
if ($id === '' || ($verwendet !== [] && !isset($verwendet[$id]))) {
|
||||
continue;
|
||||
}
|
||||
if ($verwendet === []
|
||||
&& !in_array((string) ($eintrag['Typ'] ?? ''), ['pv', 'hybrid'], true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$suffix = substr(hash('sha256', 'pv:' . $id), 0, 12);
|
||||
$leistungID = (int) ($eintrag['IstleistungVariableID'] ?? 0);
|
||||
$energieID = (int) ($eintrag['ErzeugungsenergieVariableID'] ?? 0);
|
||||
$ergebnis[] = [
|
||||
'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id,
|
||||
'LeistungVariableID' => $this->istNumerischeVariable($leistungID)
|
||||
? $leistungID
|
||||
: 0,
|
||||
'Leistungsfaktor' => (float) ($eintrag['Istleistungsfaktor'] ?? 1.0),
|
||||
'EnergieVariableID' => $this->istNumerischeVariable($energieID)
|
||||
? $energieID
|
||||
: 0,
|
||||
'Energiefaktor' => (float) ($eintrag['Erzeugungsenergiefaktor'] ?? 1.0),
|
||||
'LeistungIdent' => 'DiagrammPVLeistung_' . $suffix,
|
||||
'EnergieIdent' => 'DiagrammPVEnergie_' . $suffix,
|
||||
];
|
||||
}
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/** @return list<array<string, mixed>> */
|
||||
private function diagrammBatterien(): array
|
||||
{
|
||||
$ergebnis = [];
|
||||
foreach ($this->leseAnlagenKonfigurationsliste('AnlagenBatterien') as $eintrag) {
|
||||
$id = trim((string) ($eintrag['ID'] ?? ''));
|
||||
if ($id === '') {
|
||||
continue;
|
||||
}
|
||||
$suffix = substr(hash('sha256', 'batterie:' . $id), 0, 12);
|
||||
$leistungID = (int) ($eintrag['LeistungVariableID'] ?? 0);
|
||||
$ladenID = (int) ($eintrag['LadeenergieVariableID'] ?? 0);
|
||||
$entladenID = (int) ($eintrag['EntladeenergieVariableID'] ?? 0);
|
||||
$ergebnis[] = [
|
||||
'Name' => trim((string) ($eintrag['Name'] ?? '')) ?: $id,
|
||||
'LeistungVariableID' => $this->istNumerischeVariable($leistungID)
|
||||
? $leistungID
|
||||
: 0,
|
||||
'Leistungsfaktor' => (float) ($eintrag['Leistungsfaktor'] ?? 1.0),
|
||||
'LadeenergieVariableID' => $this->istNumerischeVariable($ladenID)
|
||||
? $ladenID
|
||||
: 0,
|
||||
'EntladeenergieVariableID' => $this->istNumerischeVariable($entladenID)
|
||||
? $entladenID
|
||||
: 0,
|
||||
'Energiefaktor' => (float) ($eintrag['Energiefaktor'] ?? 1.0),
|
||||
'LeistungIdent' => 'DiagrammBatterieLeistung_' . $suffix,
|
||||
'LadeenergieIdent' => 'DiagrammBatterieLaden_' . $suffix,
|
||||
'EntladeenergieIdent' => 'DiagrammBatterieEntladen_' . $suffix,
|
||||
];
|
||||
}
|
||||
|
||||
return $ergebnis;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $pvAnlagen
|
||||
* @param list<array<string, mixed>> $batterien
|
||||
* @param list<array<string, mixed>> $verbraucher
|
||||
*/
|
||||
private function registriereDiagrammVariablen(
|
||||
array $pvAnlagen,
|
||||
array $batterien,
|
||||
array $verbraucher
|
||||
): void {
|
||||
$position = 800;
|
||||
$this->diagrammMessvariable(
|
||||
'DiagrammHausLeistung',
|
||||
'Diagramm Hausleistung',
|
||||
'~Power',
|
||||
$position
|
||||
);
|
||||
$this->diagrammMessvariable(
|
||||
'DiagrammHausEnergie',
|
||||
'Diagramm Hausenergie',
|
||||
'~Electricity',
|
||||
$position + 1
|
||||
);
|
||||
$position += 10;
|
||||
|
||||
foreach ($pvAnlagen as $anlage) {
|
||||
if ($anlage['LeistungVariableID'] > 0) {
|
||||
$this->diagrammMessvariable(
|
||||
$anlage['LeistungIdent'],
|
||||
$anlage['Name'] . ' Leistung',
|
||||
'~Power',
|
||||
$position
|
||||
);
|
||||
}
|
||||
if ($anlage['EnergieVariableID'] > 0) {
|
||||
$this->diagrammMessvariable(
|
||||
$anlage['EnergieIdent'],
|
||||
$anlage['Name'] . ' Energie',
|
||||
'~Electricity',
|
||||
$position + 1
|
||||
);
|
||||
}
|
||||
$position += 10;
|
||||
}
|
||||
foreach ($batterien as $batterie) {
|
||||
if ($batterie['LeistungVariableID'] > 0) {
|
||||
$this->diagrammMessvariable(
|
||||
$batterie['LeistungIdent'],
|
||||
$batterie['Name'] . ' Leistung',
|
||||
'~Power',
|
||||
$position
|
||||
);
|
||||
}
|
||||
if ($batterie['LadeenergieVariableID'] > 0) {
|
||||
$this->diagrammMessvariable(
|
||||
$batterie['LadeenergieIdent'],
|
||||
$batterie['Name'] . ' Ladeenergie',
|
||||
'~Electricity',
|
||||
$position + 1
|
||||
);
|
||||
}
|
||||
if ($batterie['EntladeenergieVariableID'] > 0) {
|
||||
$this->diagrammMessvariable(
|
||||
$batterie['EntladeenergieIdent'],
|
||||
$batterie['Name'] . ' Entladeenergie',
|
||||
'~Electricity',
|
||||
$position + 2
|
||||
);
|
||||
}
|
||||
$position += 10;
|
||||
}
|
||||
foreach ($verbraucher as $eintrag) {
|
||||
$this->diagrammMessvariable(
|
||||
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
|
||||
$eintrag['Name'] . ' Leistung',
|
||||
'~Power',
|
||||
$position
|
||||
);
|
||||
$this->diagrammMessvariable(
|
||||
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
|
||||
$eintrag['Name'] . ' Energie',
|
||||
'~Electricity',
|
||||
$position + 1
|
||||
);
|
||||
$position += 10;
|
||||
}
|
||||
}
|
||||
|
||||
private function diagrammMessvariable(
|
||||
string $ident,
|
||||
string $name,
|
||||
string $profil,
|
||||
int $position
|
||||
): int {
|
||||
$this->RegisterVariableFloat($ident, $name, $profil, $position);
|
||||
$variableID = $this->GetIDForIdent($ident);
|
||||
IPS_SetHidden($variableID, true);
|
||||
|
||||
return $variableID;
|
||||
}
|
||||
|
||||
private function aktualisiereDiagrammMesswerte(bool $energieAktualisieren = false): void
|
||||
{
|
||||
if ($this->findeUnterobjekt('Leistungsdiagramm') === 0
|
||||
&& $this->findeUnterobjekt('Energiediagramm') === 0
|
||||
&& $this->findeUnterobjekt('DiagrammHausLeistung') === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$leistungen = $this->leseMessleistungen();
|
||||
$pvAnlagen = $this->diagrammPVAnlagen();
|
||||
$batterien = $this->diagrammBatterien();
|
||||
$verbraucher = $this->energieflussVerbraucher(
|
||||
'EnergiediagrammKonfiguration'
|
||||
);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->protokolliere('Energiediagramme', $fehler->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$separateLeistung = 0.0;
|
||||
$separateEnergieKWh = 0.0;
|
||||
foreach ($verbraucher as $eintrag) {
|
||||
$leistung = max(0.0, (float) $eintrag['Istleistung_W']);
|
||||
$separateLeistung += $leistung;
|
||||
$separateEnergieKWh += max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0;
|
||||
$this->setzeDiagrammMesswert(
|
||||
'DiagrammVerbraucherLeistung_' . $eintrag['InstanzID'],
|
||||
$leistung
|
||||
);
|
||||
if ($energieAktualisieren) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
'DiagrammVerbraucherEnergie_' . $eintrag['InstanzID'],
|
||||
max(0.0, (float) $eintrag['Energie_Wh']) / 1000.0
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->setzeDiagrammMesswert(
|
||||
'DiagrammHausLeistung',
|
||||
max(0.0, $leistungen['Haus'] - $separateLeistung)
|
||||
);
|
||||
if ($energieAktualisieren) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
'DiagrammHausEnergie',
|
||||
max(
|
||||
0.0,
|
||||
(float) $this->GetValue('Hausenergie') - $separateEnergieKWh
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($pvAnlagen as $anlage) {
|
||||
if ($anlage['LeistungVariableID'] > 0) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
$anlage['LeistungIdent'],
|
||||
max(
|
||||
0.0,
|
||||
(float) GetValue($anlage['LeistungVariableID'])
|
||||
* $anlage['Leistungsfaktor']
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($energieAktualisieren && $anlage['EnergieVariableID'] > 0) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
$anlage['EnergieIdent'],
|
||||
max(
|
||||
0.0,
|
||||
(float) GetValue($anlage['EnergieVariableID'])
|
||||
* $anlage['Energiefaktor']
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
foreach ($batterien as $batterie) {
|
||||
if ($batterie['LeistungVariableID'] > 0) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
$batterie['LeistungIdent'],
|
||||
(float) GetValue($batterie['LeistungVariableID'])
|
||||
* $batterie['Leistungsfaktor']
|
||||
);
|
||||
}
|
||||
if ($energieAktualisieren && $batterie['LadeenergieVariableID'] > 0) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
$batterie['LadeenergieIdent'],
|
||||
max(
|
||||
0.0,
|
||||
(float) GetValue($batterie['LadeenergieVariableID'])
|
||||
* $batterie['Energiefaktor']
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($energieAktualisieren && $batterie['EntladeenergieVariableID'] > 0) {
|
||||
$this->setzeDiagrammMesswert(
|
||||
$batterie['EntladeenergieIdent'],
|
||||
max(
|
||||
0.0,
|
||||
(float) GetValue($batterie['EntladeenergieVariableID'])
|
||||
* $batterie['Energiefaktor']
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setzeDiagrammMesswert(string $ident, float $wert): void
|
||||
{
|
||||
$variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||
if (is_int($variableID) && $variableID > 0 && IPS_VariableExists($variableID)) {
|
||||
SetValue($variableID, $wert);
|
||||
}
|
||||
}
|
||||
|
||||
private function diagrammFarbe(int $index): string
|
||||
{
|
||||
$farben = [
|
||||
'#A85D3C',
|
||||
'#007C91',
|
||||
'#6B5B95',
|
||||
'#2E7D32',
|
||||
'#C43E1C',
|
||||
'#3D5A80',
|
||||
'#8D6E63',
|
||||
'#00897B',
|
||||
];
|
||||
|
||||
return $farben[$index % count($farben)];
|
||||
}
|
||||
|
||||
/** @param list<array<string, mixed>> $datensaetze */
|
||||
private function erstelleDiagramm(
|
||||
string $ident,
|
||||
@@ -521,7 +1063,8 @@ trait ManagerEnergieTrait
|
||||
int $position
|
||||
): int {
|
||||
$medienID = $this->findeUnterobjekt($ident);
|
||||
if ($medienID === 0) {
|
||||
$neu = $medienID === 0;
|
||||
if ($neu) {
|
||||
$medienID = IPS_CreateMedia(4);
|
||||
IPS_SetParent($medienID, $this->InstanceID);
|
||||
IPS_SetIdent($medienID, $ident);
|
||||
@@ -530,6 +1073,11 @@ trait ManagerEnergieTrait
|
||||
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
|
||||
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
|
||||
}
|
||||
if (!$neu) {
|
||||
IPS_SetHidden($medienID, false);
|
||||
|
||||
return $medienID;
|
||||
}
|
||||
IPS_SetName($medienID, $name);
|
||||
IPS_SetPosition($medienID, $position);
|
||||
IPS_SetHidden($medienID, false);
|
||||
@@ -546,16 +1094,17 @@ trait ManagerEnergieTrait
|
||||
string $ident,
|
||||
string $titel,
|
||||
string $farbe,
|
||||
string $profil
|
||||
string $profil,
|
||||
string $typ = 'line'
|
||||
): array {
|
||||
return [
|
||||
'variableID' => $this->GetIDForIdent($ident),
|
||||
'fillColor' => 'clear',
|
||||
'fillColor' => $typ === 'bar' ? $farbe : 'clear',
|
||||
'strokeColor' => $farbe,
|
||||
'timeOffset' => 0,
|
||||
'visible' => true,
|
||||
'title' => $titel,
|
||||
'type' => 'line',
|
||||
'type' => $typ,
|
||||
'side' => 'left',
|
||||
'profile' => $profil,
|
||||
];
|
||||
|
||||
+96
-7
@@ -208,6 +208,7 @@ final class ManagerRegler
|
||||
'InstanzID' => $eintrag['InstanzID'],
|
||||
'Prioritaet' => $daten[$prioritaetsfeld],
|
||||
'Energiegruppe' => (int) round((float) $energie / 2000.0),
|
||||
'Energie_Wh' => (float) $energie,
|
||||
'Leistungswerte_W' => $daten['Leistungswerte_W'],
|
||||
'Festwert' => $festwert,
|
||||
'Festgehalten' => $festgehalten,
|
||||
@@ -254,6 +255,7 @@ final class ManagerRegler
|
||||
}
|
||||
|
||||
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
|
||||
$gruppen = [];
|
||||
foreach ($steuerbar as $eintrag) {
|
||||
if ($eintrag['Festwert'] || $eintrag['Festgehalten']) {
|
||||
continue;
|
||||
@@ -262,13 +264,8 @@ final class ManagerRegler
|
||||
$instanzID = $eintrag['InstanzID'];
|
||||
$ausgangswert = $sollwerte[$instanzID];
|
||||
if ($positiveRichtung) {
|
||||
if ($rest <= 0) {
|
||||
continue;
|
||||
}
|
||||
$neuerSollwert = self::groessterLeistungswertBis(
|
||||
$eintrag['Leistungswerte_W'],
|
||||
$ausgangswert + $rest
|
||||
);
|
||||
$gruppen[$eintrag['Prioritaet']][] = $eintrag;
|
||||
continue;
|
||||
} else {
|
||||
if ($rest >= 0) {
|
||||
continue;
|
||||
@@ -281,6 +278,9 @@ final class ManagerRegler
|
||||
$rest -= $neuerSollwert - $ausgangswert;
|
||||
$sollwerte[$instanzID] = $neuerSollwert;
|
||||
}
|
||||
foreach ($gruppen as $gruppe) {
|
||||
self::verteilePositiveGruppe($gruppe, $sollwerte, $rest);
|
||||
}
|
||||
|
||||
$zugeteilt = $nichtSteuerbar;
|
||||
foreach ($steuerbar as $eintrag) {
|
||||
@@ -298,6 +298,95 @@ final class ManagerRegler
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $gruppe Verbraucher derselben Prioritaet
|
||||
* @param array<int, int> $sollwerte
|
||||
*/
|
||||
private static function verteilePositiveGruppe(array $gruppe, array &$sollwerte, int &$rest): void
|
||||
{
|
||||
while ($rest > 0) {
|
||||
$kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest);
|
||||
if ($kandidaten === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ganze Leistungsniveaus gemeinsam vergeben, ohne Bereiche Watt fuer Watt zu durchlaufen.
|
||||
$unten = $kandidaten[0]['NaechsteLeistung_W'] - 1;
|
||||
$oben = $unten;
|
||||
foreach ($kandidaten as $eintrag) {
|
||||
$oben = max($oben, $sollwerte[$eintrag['InstanzID']] + $rest);
|
||||
}
|
||||
while ($unten < $oben) {
|
||||
$mitte = $unten + intdiv($oben - $unten + 1, 2);
|
||||
$bedarf = 0;
|
||||
foreach ($kandidaten as $eintrag) {
|
||||
$bisher = $sollwerte[$eintrag['InstanzID']];
|
||||
$bedarf += max(0, self::groessterLeistungswertBis(
|
||||
$eintrag['Leistungswerte_W'],
|
||||
$mitte
|
||||
) - $bisher);
|
||||
if ($bedarf > $rest) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($bedarf <= $rest) {
|
||||
$unten = $mitte;
|
||||
} else {
|
||||
$oben = $mitte - 1;
|
||||
}
|
||||
}
|
||||
foreach ($kandidaten as $eintrag) {
|
||||
$id = $eintrag['InstanzID'];
|
||||
$neu = max($sollwerte[$id], self::groessterLeistungswertBis(
|
||||
$eintrag['Leistungswerte_W'],
|
||||
$unten
|
||||
));
|
||||
$rest -= $neu - $sollwerte[$id];
|
||||
$sollwerte[$id] = $neu;
|
||||
}
|
||||
|
||||
// Reicht es nicht fuer das ganze naechste Niveau, entscheidet Leistung, Energie, ID.
|
||||
$kandidaten = self::naechsteLeistungsschritte($gruppe, $sollwerte, $rest);
|
||||
if ($kandidaten === []) {
|
||||
return;
|
||||
}
|
||||
$erster = $kandidaten[0];
|
||||
$id = $erster['InstanzID'];
|
||||
$rest -= $erster['NaechsteLeistung_W'] - $sollwerte[$id];
|
||||
$sollwerte[$id] = $erster['NaechsteLeistung_W'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $gruppe
|
||||
* @param array<int, int> $sollwerte
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private static function naechsteLeistungsschritte(array $gruppe, array $sollwerte, int $rest): array
|
||||
{
|
||||
$kandidaten = [];
|
||||
foreach ($gruppe as $eintrag) {
|
||||
$bisher = $sollwerte[$eintrag['InstanzID']];
|
||||
foreach ($eintrag['Leistungswerte_W'] as $angebot) {
|
||||
$naechste = is_int($angebot) ? $angebot : max($angebot['Von_W'], $bisher + 1);
|
||||
if ($naechste <= $bisher || (!is_int($angebot) && $naechste > $angebot['Bis_W'])) {
|
||||
continue;
|
||||
}
|
||||
if ($naechste - $bisher <= $rest) {
|
||||
$eintrag['NaechsteLeistung_W'] = $naechste;
|
||||
$kandidaten[] = $eintrag;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
usort($kandidaten, static function (array $links, array $rechts): int {
|
||||
return [$links['NaechsteLeistung_W'], $links['Energie_Wh'], $links['InstanzID']]
|
||||
<=> [$rechts['NaechsteLeistung_W'], $rechts['Energie_Wh'], $rechts['InstanzID']];
|
||||
});
|
||||
|
||||
return $kandidaten;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $daten
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,30 @@ final class BatterieModulstrukturTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testNachladebegrenzungIstImFormularKonfigurierbar(): void
|
||||
{
|
||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||
|
||||
self::assertStringContainsString(
|
||||
"RegisterPropertyBoolean('NachladenMitMaximalleistung', true)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"RegisterPropertyInteger('MaximaleNachladeleistung', 0)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"'FormNachladenMitMaximalleistung'",
|
||||
$formular
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
'"name": "MaximaleNachladeleistung"',
|
||||
$formular
|
||||
);
|
||||
self::assertStringContainsString('"suffix": " W"', $formular);
|
||||
}
|
||||
|
||||
public function testLoggingDiagnoseUndAktionsbuttonsSindVorhanden(): void
|
||||
{
|
||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||
|
||||
@@ -82,6 +82,38 @@ final class BatterieReglerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testNachladeleistungBegrenztNurSchutzbedingtesLaden(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[400],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 19.0, 20.0, 10.0, true,
|
||||
0.0, 0.0, 1000.0, 1000.0, 400.0
|
||||
)
|
||||
);
|
||||
self::assertSame(
|
||||
[['Von_W' => 0, 'Bis_W' => 400]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, true, 10.0, 20.0, 10.0, true,
|
||||
0.0, 0.0, 1000.0, 1000.0, 400.0
|
||||
)
|
||||
);
|
||||
self::assertSame(
|
||||
[['Von_W' => -1000, 'Bis_W' => 400]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, true, 15.0, 20.0, 10.0, true,
|
||||
0.0, 0.0, 1000.0, 1000.0, 400.0
|
||||
)
|
||||
);
|
||||
self::assertSame(
|
||||
[['Von_W' => -1000, 'Bis_W' => 1000]],
|
||||
BatterieRegler::leistungsangebot(
|
||||
true, true, false, 50.0, 20.0, 10.0, false,
|
||||
0.0, 0.0, 1000.0, 1000.0, 400.0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testWechselrichtersteuerungBietetNurNull(): void
|
||||
{
|
||||
self::assertSame(
|
||||
|
||||
@@ -70,6 +70,19 @@ final class EaseeModuleStrukturTest extends TestCase
|
||||
self::assertStringContainsString("'Phasenzahl'", $modul);
|
||||
self::assertStringNotContainsString('7500', $modul);
|
||||
self::assertStringNotContainsString('Abfrageintervall', $modul);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableFloat('Istleistung', 'Istleistung', '', 170)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString('RegeluebergangTimeout', $modul);
|
||||
self::assertStringContainsString("'action' => 'StartCharging'", $modul);
|
||||
|
||||
$gateway = (string) file_get_contents(
|
||||
|
||||
@@ -13,7 +13,7 @@ final class LadestationAdapterTest extends TestCase
|
||||
/**
|
||||
* @dataProvider statusFaelle
|
||||
* @param array<string, string> $konfiguration
|
||||
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $erwartet
|
||||
* @param array{Rohstatus: int, Istleistung_W: float, Sollstrom_A?: int, Maximalstrom_A?: int, Iststrom_A?: float, Phasenzahl?: int} $erwartet
|
||||
*/
|
||||
public function testStatusaufrufUndDekodierungFuerAlleGeraete(
|
||||
int $geraetetyp,
|
||||
@@ -52,6 +52,8 @@ final class LadestationAdapterTest extends TestCase
|
||||
[
|
||||
'Rohstatus' => 2,
|
||||
'Istleistung_W' => 11000.0,
|
||||
'Sollstrom_A' => 16,
|
||||
'Maximalstrom_A' => 16,
|
||||
'Iststrom_A' => 16.0,
|
||||
'Phasenzahl' => 3,
|
||||
],
|
||||
@@ -64,6 +66,8 @@ final class LadestationAdapterTest extends TestCase
|
||||
[
|
||||
'Rohstatus' => 2,
|
||||
'Istleistung_W' => 2300.0,
|
||||
'Sollstrom_A' => 16,
|
||||
'Maximalstrom_A' => 16,
|
||||
'Iststrom_A' => 9.8,
|
||||
'Phasenzahl' => 1,
|
||||
],
|
||||
@@ -81,6 +85,20 @@ final class LadestationAdapterTest extends TestCase
|
||||
];
|
||||
}
|
||||
|
||||
public function testGoEAusFreigabeWirdAlsNullSollstromDekodiert(): void
|
||||
{
|
||||
$daten = json_decode($this->goEAntwort(2, 0, [0, 0, 0]), true, 512, JSON_THROW_ON_ERROR);
|
||||
$daten['alw'] = false;
|
||||
$daten['frc'] = 1;
|
||||
|
||||
$status = LadestationAdapter::dekodiereStatus(
|
||||
LadestationAdapter::GO_E_GEMINI,
|
||||
json_encode($daten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
self::assertSame(0, $status['Sollstrom_A']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider steuerFaelle
|
||||
* @param array<string, string> $konfiguration
|
||||
@@ -180,6 +198,13 @@ final class LadestationAdapterTest extends TestCase
|
||||
}
|
||||
$nrg[11] = $leistung;
|
||||
|
||||
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
||||
return json_encode([
|
||||
'car' => $status,
|
||||
'nrg' => $nrg,
|
||||
'amp' => 16,
|
||||
'ama' => 16,
|
||||
'alw' => true,
|
||||
'frc' => 2,
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,24 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LadestationReglerTest extends TestCase
|
||||
{
|
||||
public function testPhasenprobeUnterscheidetBeiMindeststromOhneMaximalstromsprung(): void
|
||||
{
|
||||
self::assertSame(
|
||||
LadestationRegler::PHASEN_EIN,
|
||||
LadestationRegler::erkennePhasenzahlAusLeistung(1380.0, 6)
|
||||
);
|
||||
self::assertSame(
|
||||
LadestationRegler::PHASEN_DREI,
|
||||
LadestationRegler::erkennePhasenzahlAusLeistung(4104.0, 6)
|
||||
);
|
||||
}
|
||||
|
||||
public function testPhasenprobeWeistUngueltigenErkennungsstromAb(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
LadestationRegler::erkennePhasenzahlAusLeistung(4104.0, 5);
|
||||
}
|
||||
|
||||
public function testFahrzeugUndDreiPhasenWerdenWieInEnelixEinsErkannt(): void
|
||||
{
|
||||
$status = LadestationRegler::normalisiereStatus([
|
||||
|
||||
@@ -88,6 +88,48 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
||||
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||
self::assertStringContainsString("ReadPropertyBoolean('EinstellungenInVisu')", $modul);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableFloat('Istleistung', 'Istleistung', '', 170)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString(
|
||||
"RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString('RegeluebergangTimeout', $modul);
|
||||
self::assertStringContainsString('PHASENERKENNUNGSSTROM = 6', $modul);
|
||||
self::assertStringContainsString('UnregisterVariable($ident)', $modul);
|
||||
}
|
||||
|
||||
public function testMeldezyklusSendetDenBereitsGelesenenZustandOhneZusaetzlichenHttpAbruf(): void
|
||||
{
|
||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||
self::assertSame(1, preg_match(
|
||||
"/case 'Melden':(?<block>.*?)case 'ManagerdatenEmpfangen':/s",
|
||||
$modul,
|
||||
$treffer
|
||||
));
|
||||
self::assertStringContainsString('$this->sendeVerbraucherdaten();', $treffer['block']);
|
||||
self::assertStringNotContainsString('$this->aktualisiere(', $treffer['block']);
|
||||
}
|
||||
|
||||
public function testVomGeraetBestaetigterSollstromWirdFuerWiederholungVerwendet(): void
|
||||
{
|
||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||
|
||||
self::assertStringContainsString("\$rohstatus['Sollstrom_A']", $modul);
|
||||
self::assertStringContainsString(
|
||||
"WriteAttributeInteger('LetzterGesetzterStrom', \$geraeteSollstrom)",
|
||||
$modul
|
||||
);
|
||||
self::assertStringContainsString("RegisterAttributeInteger('GeraeteMaximalstrom', 32)", $modul);
|
||||
self::assertStringContainsString(
|
||||
"WriteAttributeInteger('GeraeteMaximalstrom', (int) \$rohstatus['Maximalstrom_A'])",
|
||||
$modul
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
||||
foreach ([
|
||||
"RegisterPropertyBoolean('EnergyPieAnzeigen', false)",
|
||||
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
|
||||
"RegisterPropertyInteger('DiagrammPVModus', 0)",
|
||||
"RegisterPropertyInteger('DiagrammBatterieModus', 0)",
|
||||
"RegisterPropertyBoolean('LadestationenSeparatAnzeigen', true)",
|
||||
"RegisterPropertyBoolean('VerbraucherSeparatAnzeigen', true)",
|
||||
"RegisterAttributeBoolean('EnergieflussNeuErzeugen', false)",
|
||||
"RegisterPropertyBoolean('FunFactsAnzeigen', false)",
|
||||
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
|
||||
"RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)",
|
||||
@@ -38,6 +43,10 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
||||
'ENERGY_PIE_MODULE_ID',
|
||||
'ENERGY_FLOW_MODULE_ID',
|
||||
'energieflussKnoten',
|
||||
"'type' => \$typ",
|
||||
"'bar'",
|
||||
'EnergiediagrammKonfiguration',
|
||||
'EnergieflussKonfiguration',
|
||||
'IPS_DeleteInstance',
|
||||
'IPS_DeleteMedia',
|
||||
] as $erwartet) {
|
||||
@@ -45,11 +54,14 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
||||
}
|
||||
foreach ([
|
||||
'Energieaufzeichnung',
|
||||
'FormEnergyPieAnzeigen',
|
||||
'FormEnergiediagrammeAnzeigen',
|
||||
'FormFunFactsAnzeigen',
|
||||
'FormEnergieflussAnzeigen',
|
||||
'EnergieflussLeistungseinheit',
|
||||
'DiagrammPVModus',
|
||||
'DiagrammBatterieModus',
|
||||
'LadestationenSeparatAnzeigen',
|
||||
'VerbraucherSeparatAnzeigen',
|
||||
'Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert',
|
||||
'Wechselrichter / AC-Wandler',
|
||||
'keine doppelte Batteriekonfiguration',
|
||||
'MesswerteAnzeigen',
|
||||
'IstleistungVariableID',
|
||||
'LeistungVariableID',
|
||||
|
||||
@@ -380,4 +380,25 @@ final class ManagerModulstrukturTest extends TestCase
|
||||
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
|
||||
self::assertStringContainsString("'ManagerdatenEmpfangen'", $inhalt);
|
||||
}
|
||||
|
||||
public function testVerbrauchermeldungPlantRegelungOhneSynchronenRueckruf(): void
|
||||
{
|
||||
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
|
||||
self::assertStringContainsString(
|
||||
"RegisterTimer(\n 'RegelungVerzoegert'",
|
||||
$inhalt
|
||||
);
|
||||
self::assertStringContainsString("case 'RegelnVerzoegert':", $inhalt);
|
||||
self::assertSame(1, preg_match(
|
||||
'/public function VerbraucherdatenEmpfangen\(array \$daten\): void(?<block>.*?)'
|
||||
. 'private function aktualisiereVerbraucherEnergie/s',
|
||||
$inhalt,
|
||||
$treffer
|
||||
));
|
||||
self::assertStringContainsString(
|
||||
"SetTimerInterval('RegelungVerzoegert', 100)",
|
||||
$treffer['block']
|
||||
);
|
||||
self::assertStringNotContainsString('$this->regeln(false);', $treffer['block']);
|
||||
}
|
||||
}
|
||||
|
||||
+170
-3
@@ -166,7 +166,7 @@ final class ManagerReglerTest extends TestCase
|
||||
self::assertSame(500, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testGleichePrioritaetBevorzugtWieEnelix1DenVerbraucherMitWenigerEnergie(): void
|
||||
public function testGleicheNaechsteLeistungBevorzugtWenigerEnergie(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(
|
||||
0.0,
|
||||
@@ -183,7 +183,7 @@ final class ManagerReglerTest extends TestCase
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testEnergievergleichVerwendetWieEnelix1ZweiKilowattstundenGruppen(): void
|
||||
public function testEnergiegleichstandVergleichtExakteWhStattZweiKilowattstundenGruppen(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(
|
||||
0.0,
|
||||
@@ -196,7 +196,174 @@ final class ManagerReglerTest extends TestCase
|
||||
[10 => 999.0, 20 => 0.0]
|
||||
);
|
||||
|
||||
self::assertSame([10 => 3000, 20 => 0], $ergebnis['Sollwerte']);
|
||||
self::assertSame([10 => 0, 20 => 3000], $ergebnis['Sollwerte']);
|
||||
}
|
||||
|
||||
/** @dataProvider beispielBudgets */
|
||||
public function testBestaetigteStufenfolge(int $budget, int $a, int $b): void
|
||||
{
|
||||
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $feld) {
|
||||
$ergebnis = ManagerRegler::verteile(0.0, (float) $budget, [
|
||||
$this->verbraucher(20, 2, [0, 100, 110, 500, 780, 1500], 0),
|
||||
$this->verbraucher(10, 2, [0, 100, 200, 400, 800], 0),
|
||||
], $feld, [10 => 10000.0, 20 => 12000.0]);
|
||||
|
||||
self::assertSame([10 => $a, 20 => $b], $ergebnis['Sollwerte']);
|
||||
self::assertSame($budget - $a - $b, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
}
|
||||
|
||||
public function beispielBudgets(): array
|
||||
{
|
||||
return [
|
||||
[0, 0, 0], [99, 0, 0], [100, 100, 0], [199, 100, 0],
|
||||
[200, 100, 100], [209, 100, 100], [210, 100, 110],
|
||||
[309, 100, 110], [310, 200, 110], [509, 200, 110],
|
||||
[510, 400, 110], [899, 400, 110], [900, 400, 500],
|
||||
[1179, 400, 500], [1180, 400, 780], [1579, 400, 780],
|
||||
[1580, 800, 780], [2299, 800, 780], [2300, 800, 1500],
|
||||
[3000, 800, 1500],
|
||||
];
|
||||
}
|
||||
|
||||
public function testLeistungsniveauGehtVorEnergieUndSchrittdifferenz(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 220.0, [
|
||||
$this->verbraucher(10, 0, [0, 100, 200], 0),
|
||||
$this->verbraucher(20, 0, [0, 110, 120], 0),
|
||||
], 'PrioritaetPV', [10 => 0.0, 20 => 50000.0]);
|
||||
|
||||
self::assertSame([10 => 100, 20 => 120], $ergebnis['Sollwerte']);
|
||||
}
|
||||
|
||||
public function testPicoUndGoETeilenBudgetTrotzGrossemEnergieunterschied(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 12312.0, [
|
||||
$this->verbraucher(31986, 2, array_merge([0], range(4104, 12312, 684)), 0),
|
||||
$this->verbraucher(43878, 2, array_merge([0], range(4104, 10944, 684)), 0),
|
||||
], 'PrioritaetPV', [31986 => 41192.78, 43878 => 128082.87]);
|
||||
|
||||
self::assertSame([31986 => 6156, 43878 => 6156], $ergebnis['Sollwerte']);
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testNegativeDefizitverteilungBehaeltBisherigeEnergiegruppen(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(300.0, 0.0, [
|
||||
$this->verbraucher(10, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0),
|
||||
$this->verbraucher(20, 0, [['Von_W' => -300, 'Bis_W' => 300]], 0),
|
||||
], 'PrioritaetPeak', [10 => 999.0, 20 => 0.0]);
|
||||
|
||||
self::assertSame([10 => -300, 20 => 0], $ergebnis['Sollwerte']);
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testGleicheLeistungUndEnergieEntscheidetStabilNachInstanzID(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 100.0, [
|
||||
$this->verbraucher(20, 0, [0, 100], 0),
|
||||
$this->verbraucher(10, 0, [0, 100], 0),
|
||||
], 'PrioritaetPV');
|
||||
|
||||
self::assertSame([10 => 100, 20 => 0], $ergebnis['Sollwerte']);
|
||||
}
|
||||
|
||||
public function testNichtPassenderSchrittBlockiertKeineAndereErreichbareStufe(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 210.0, [
|
||||
$this->verbraucher(10, 0, [0, 100, 200], 0),
|
||||
$this->verbraucher(20, 0, [0, 150], 0),
|
||||
], 'PrioritaetPV');
|
||||
|
||||
self::assertSame([10 => 200, 20 => 0], $ergebnis['Sollwerte']);
|
||||
self::assertSame(10, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testPeakVerwendetPeakPrioritaetVorLeistungUndEnergie(): void
|
||||
{
|
||||
$a = $this->verbraucher(10, 0, [0, 100, 200], 0);
|
||||
$b = $this->verbraucher(20, 9, [0, 300], 0);
|
||||
$a['Daten']['PrioritaetPeak'] = 9;
|
||||
$b['Daten']['PrioritaetPeak'] = 0;
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 300.0, [$a, $b], 'PrioritaetPeak', [20 => 50000.0]);
|
||||
|
||||
self::assertSame([20 => 300, 10 => 0], $ergebnis['Sollwerte']);
|
||||
}
|
||||
|
||||
public function testGrosseBereicheWerdenGemeinsamOhneWattweiseSchleifeVerteilt(): void
|
||||
{
|
||||
$ergebnis = ManagerRegler::verteile(0.0, 1000000001.0, [
|
||||
$this->verbraucher(10, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0),
|
||||
$this->verbraucher(20, 0, [['Von_W' => 0, 'Bis_W' => 1000000000]], 0),
|
||||
], 'PrioritaetPV', [10 => 999.0, 20 => 0.0]);
|
||||
|
||||
self::assertSame([10 => 500000000, 20 => 500000001], $ergebnis['Sollwerte']);
|
||||
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
|
||||
public function testBereicheUndEinzelwerteGegenWattweiseReferenz(): void
|
||||
{
|
||||
$angebote = [
|
||||
[0, ['Von_W' => 3, 'Bis_W' => 9], 15],
|
||||
[0, 5, ['Von_W' => 7, 'Bis_W' => 12], 18],
|
||||
[0, 4, 8, ['Von_W' => 14, 'Bis_W' => 20]],
|
||||
];
|
||||
$energien = [10 => 10000.5, 20 => 10000.0, 30 => 10000.0];
|
||||
$verbraucher = [];
|
||||
$diskret = [];
|
||||
foreach ($angebote as $index => $angebot) {
|
||||
$id = ($index + 1) * 10;
|
||||
$verbraucher[] = $this->verbraucher($id, 2, $angebot, 0);
|
||||
$diskret[$id] = [];
|
||||
foreach ($angebot as $wert) {
|
||||
foreach (is_int($wert) ? [$wert] : range($wert['Von_W'], $wert['Bis_W']) as $watt) {
|
||||
$diskret[$id][] = $watt;
|
||||
}
|
||||
}
|
||||
}
|
||||
for ($budget = 0; $budget <= 60; $budget++) {
|
||||
$erwartet = [10 => 0, 20 => 0, 30 => 0];
|
||||
$rest = $budget;
|
||||
while (true) {
|
||||
$schritte = [];
|
||||
foreach ($diskret as $id => $werte) {
|
||||
foreach ($werte as $wert) {
|
||||
if ($wert > $erwartet[$id]) {
|
||||
if ($wert - $erwartet[$id] <= $rest) {
|
||||
$schritte[] = [$wert, $energien[$id], $id];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($schritte === []) {
|
||||
break;
|
||||
}
|
||||
sort($schritte);
|
||||
[$wert, , $id] = $schritte[0];
|
||||
$rest -= $wert - $erwartet[$id];
|
||||
$erwartet[$id] = $wert;
|
||||
}
|
||||
$ergebnis = ManagerRegler::verteile(0.0, (float) $budget, array_reverse($verbraucher),
|
||||
'PrioritaetPV', $energien);
|
||||
$ist = $ergebnis['Sollwerte'];
|
||||
ksort($ist);
|
||||
self::assertSame($erwartet, $ist, 'Budget: ' . $budget);
|
||||
self::assertSame($rest, $ergebnis['Restabweichung_W']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testNichtVerfuegbareLastWirdReserviertUndNichtNeuZugeteilt(): void
|
||||
{
|
||||
$a = $this->verbraucher(10, 0, [0, 100, 200], 100);
|
||||
$a['Daten']['Verfuegbar'] = false;
|
||||
$ergebnis = ManagerRegler::verteile(100.0, 200.0, [
|
||||
$a,
|
||||
$this->verbraucher(20, 0, [0, 100, 200], 0),
|
||||
], 'PrioritaetPV');
|
||||
|
||||
self::assertSame(100, $ergebnis['NichtSteuerbar_W']);
|
||||
self::assertSame([20 => 100], $ergebnis['Sollwerte']);
|
||||
}
|
||||
|
||||
public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
|
||||
|
||||
@@ -163,6 +163,32 @@ PHP
|
||||
}
|
||||
);
|
||||
|
||||
$test->runCase(
|
||||
'Benutzerdefinierte Nachladeleistung begrenzt das Schutzangebot',
|
||||
static function (TestContext $test) use (
|
||||
$batterieID,
|
||||
$ladezustandID,
|
||||
$managerdatenSenden
|
||||
): void {
|
||||
$test->configureInstance($batterieID, [
|
||||
'Batteriemanagement' => 2,
|
||||
'NachladenMitMaximalleistung' => false,
|
||||
'MaximaleNachladeleistung' => 400,
|
||||
]);
|
||||
IPS_RequestAction($batterieID, 'Aktiv', true);
|
||||
SetValue($ladezustandID, 19.0);
|
||||
$managerdatenSenden('PV', null);
|
||||
|
||||
$angebotID = $test->objectByIdent('LeistungsangebotDiagnose', $batterieID);
|
||||
$test->waitUntil(
|
||||
static fn (): bool => json_decode(
|
||||
(string) GetValue($angebotID),
|
||||
true
|
||||
) === [400]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$test->runCase(
|
||||
'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus',
|
||||
static function (TestContext $test) use (
|
||||
|
||||
@@ -184,6 +184,58 @@ return static function (TestContext $test): void {
|
||||
$test->assertInstanceStatus($station);
|
||||
});
|
||||
|
||||
$test->runCase('Solarumschaltung wartet auf die neue Manager-Vorgabe', static function (
|
||||
TestContext $test
|
||||
) use ($station): void {
|
||||
$managerId = $test->createInstance(
|
||||
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||
'Hilfsmanager Easee-Uebergang',
|
||||
[
|
||||
'AutomatischeSuche' => false,
|
||||
'VerbraucherZuordnung' => json_encode([
|
||||
['InstanzID' => $station, 'Aktiv' => true],
|
||||
], JSON_THROW_ON_ERROR),
|
||||
]
|
||||
);
|
||||
$sendeSollwert = static function (int $leistung) use ($managerId, $station): void {
|
||||
IPS_RequestAction($station, 'ManagerdatenEmpfangen', json_encode([
|
||||
'Kopf' => [
|
||||
'Version' => '4.0',
|
||||
'AbsenderID' => $managerId,
|
||||
'EmpfaengerID' => $station,
|
||||
'Zeitpunkt' => time(),
|
||||
],
|
||||
'Betriebsart' => 'PV',
|
||||
'Sollleistung_W' => $leistung,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
};
|
||||
|
||||
$sendeSollwert(4104);
|
||||
$befehlVorUmschaltung = (string) GetValue(
|
||||
$test->objectByIdent('LetzterGeraetebefehl', $station)
|
||||
);
|
||||
IPS_RequestAction($station, 'Solarladen', false);
|
||||
|
||||
$test->assertSame(
|
||||
$befehlVorUmschaltung,
|
||||
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station))
|
||||
);
|
||||
$test->assertSame(
|
||||
4104,
|
||||
GetValue($test->objectByIdent('Sollleistung', $station)),
|
||||
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.'
|
||||
);
|
||||
|
||||
$sendeSollwert(10944);
|
||||
$test->assertTrue(str_contains(
|
||||
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)),
|
||||
'"Ampere":16'
|
||||
));
|
||||
|
||||
IPS_RequestAction($station, 'Solarladen', true);
|
||||
$sendeSollwert(0);
|
||||
});
|
||||
|
||||
$test->runCase('SignalR-Abbruch erreicht die Ladestation', static function (
|
||||
TestContext $test
|
||||
) use ($gateway, $station): void {
|
||||
@@ -226,4 +278,24 @@ return static function (TestContext $test): void {
|
||||
);
|
||||
$test->assertInstanceStatus($station, 202);
|
||||
});
|
||||
|
||||
$test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function (
|
||||
TestContext $test
|
||||
) use ($station): void {
|
||||
IPS_SetProperty($station, 'EinstellungenInVisu', true);
|
||||
IPS_SetProperty($station, 'DiagnosevariablenAnzeigen', false);
|
||||
IPS_ApplyChanges($station);
|
||||
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Istleistung', $station) !== false);
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Sollleistung', $station) !== false);
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Fahrzeugstatus', $station));
|
||||
|
||||
IPS_SetProperty($station, 'EinstellungenInVisu', false);
|
||||
IPS_SetProperty($station, 'DiagnosevariablenAnzeigen', true);
|
||||
IPS_ApplyChanges($station);
|
||||
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Istleistung', $station));
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Sollleistung', $station));
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Fahrzeugstatus', $station) !== false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -209,10 +209,25 @@ return static function (TestContext $test): void {
|
||||
'Sollleistung_W' => $leistung,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
};
|
||||
$warteAufGeraetebefehl = static function (
|
||||
int $stationId,
|
||||
string $urlTeil
|
||||
) use ($test): string {
|
||||
$deadline = microtime(true) + 3.0;
|
||||
do {
|
||||
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||
if (str_contains($befehl, $urlTeil)) {
|
||||
return $befehl;
|
||||
}
|
||||
IPS_Sleep(50);
|
||||
} while (microtime(true) < $deadline);
|
||||
|
||||
return $befehl;
|
||||
};
|
||||
|
||||
$test->runCase('Alle drei Geraetevarianten erkennen Fahrzeug und Phasen', static function (
|
||||
TestContext $test
|
||||
) use ($definitionen, $stationen): void {
|
||||
) use ($definitionen, $stationen, $warteAufGeraetebefehl): void {
|
||||
foreach ($stationen as $name => $stationId) {
|
||||
IPS_RequestAction($stationId, 'Aktiv', true);
|
||||
$test->assertSame(true, GetValue($test->objectByIdent('FahrzeugVerbunden', $stationId)));
|
||||
@@ -222,7 +237,10 @@ return static function (TestContext $test): void {
|
||||
);
|
||||
$test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)));
|
||||
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||
$befehl = $warteAufGeraetebefehl(
|
||||
$stationId,
|
||||
$definitionen[$name]['BefehlUrlTeil']
|
||||
);
|
||||
$diagnose = json_encode([
|
||||
'Aktiv' => GetValue($test->objectByIdent('Aktiv', $stationId)),
|
||||
'Ladefreigabe' => GetValue($test->objectByIdent('Ladefreigabe', $stationId)),
|
||||
@@ -361,7 +379,18 @@ return static function (TestContext $test): void {
|
||||
'amp=6'
|
||||
), 'Solarladen: Der 6-A-Befehl fehlt. Erhalten: ' . $befehl . '; Diagnose: ' . $diagnose);
|
||||
|
||||
$befehlVorUmschaltung = $befehl;
|
||||
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||
$test->assertSame(
|
||||
$befehlVorUmschaltung,
|
||||
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||
'Die Umschaltung darf vor der neuen Manager-Vorgabe keinen Zwischenwert setzen.'
|
||||
);
|
||||
$test->assertSame(
|
||||
4104,
|
||||
GetValue($test->objectByIdent('Sollleistung', $stationId)),
|
||||
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.'
|
||||
);
|
||||
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
|
||||
$test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
|
||||
$abgewiesen = false;
|
||||
@@ -422,7 +451,7 @@ return static function (TestContext $test): void {
|
||||
));
|
||||
});
|
||||
|
||||
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Maximalstrom erkannt', static function (
|
||||
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Mindeststrom erkannt', static function (
|
||||
TestContext $test
|
||||
) use ($phasenerkennung, $anforderungSenden): void {
|
||||
$stationId = $phasenerkennung;
|
||||
@@ -432,8 +461,8 @@ return static function (TestContext $test): void {
|
||||
);
|
||||
$test->assertTrue(!str_contains(
|
||||
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||
'amp=16'
|
||||
), 'Eine inaktive Station darf keine Maximalstrom-Probe starten.');
|
||||
'amp=6'
|
||||
), 'Eine inaktive Station darf keine Phasenprobe starten.');
|
||||
|
||||
IPS_RequestAction($stationId, 'Aktiv', true);
|
||||
|
||||
@@ -443,8 +472,8 @@ return static function (TestContext $test): void {
|
||||
);
|
||||
$test->assertTrue(str_contains(
|
||||
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||
'amp=16'
|
||||
), 'Nach der Freigabe muss die Maximalstrom-Probe starten.');
|
||||
'amp=6'
|
||||
), 'Nach der Freigabe muss die Mindeststrom-Phasenprobe starten.');
|
||||
|
||||
IPS_Sleep(2100);
|
||||
IPS_RequestAction($stationId, 'Statuszyklus', false);
|
||||
@@ -472,4 +501,25 @@ return static function (TestContext $test): void {
|
||||
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
||||
);
|
||||
});
|
||||
|
||||
$test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function (
|
||||
TestContext $test
|
||||
) use ($stationen): void {
|
||||
$stationId = $stationen['go-e alt'];
|
||||
IPS_SetProperty($stationId, 'EinstellungenInVisu', true);
|
||||
IPS_SetProperty($stationId, 'DiagnosevariablenAnzeigen', false);
|
||||
IPS_ApplyChanges($stationId);
|
||||
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Istleistung', $stationId) !== false);
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Sollleistung', $stationId) !== false);
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Fahrzeugstatus', $stationId));
|
||||
|
||||
IPS_SetProperty($stationId, 'EinstellungenInVisu', false);
|
||||
IPS_SetProperty($stationId, 'DiagnosevariablenAnzeigen', true);
|
||||
IPS_ApplyChanges($stationId);
|
||||
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Istleistung', $stationId));
|
||||
$test->assertSame(false, @IPS_GetObjectIDByIdent('Sollleistung', $stationId));
|
||||
$test->assertTrue(@IPS_GetObjectIDByIdent('Fahrzeugstatus', $stationId) !== false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,8 +6,13 @@ use Belevo\EnelixEMS\SymconTest\TestContext;
|
||||
|
||||
return static function (TestContext $test): void {
|
||||
$netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0);
|
||||
$netzbezugEnergieId = $test->createVariable(2, 'Netzbezug-Energie', 32.0);
|
||||
$netzeinspeisungEnergieId = $test->createVariable(2, 'Netzeinspeisung-Energie', 16.0);
|
||||
$pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0);
|
||||
$pvEnergieId = $test->createVariable(2, 'PV-Energie', 120.0);
|
||||
$batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.0);
|
||||
$batterieLadeenergieId = $test->createVariable(2, 'Batterie-Ladeenergie', 18.0);
|
||||
$batterieEntladeenergieId = $test->createVariable(2, 'Batterie-Entladeenergie', 12.0);
|
||||
$batterieSocId = $test->createVariable(2, 'Batterie-Ladezustand', 62.0);
|
||||
$einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig');
|
||||
$einStufigId = $test->createInstance(
|
||||
@@ -82,6 +87,8 @@ return static function (TestContext $test): void {
|
||||
'ACNennleistung_kW' => 10.0,
|
||||
'IstleistungVariableID' => $pvLeistungId,
|
||||
'Istleistungsfaktor' => 1.0,
|
||||
'ErzeugungsenergieVariableID' => $pvEnergieId,
|
||||
'Erzeugungsenergiefaktor' => 1.0,
|
||||
]];
|
||||
$pvKonfiguration = [[
|
||||
'ID' => 'pv-1',
|
||||
@@ -106,6 +113,9 @@ return static function (TestContext $test): void {
|
||||
'LeistungVariableID' => $batterieLeistungId,
|
||||
'Leistungsfaktor' => 1.0,
|
||||
'SOCVariableID' => $batterieSocId,
|
||||
'LadeenergieVariableID' => $batterieLadeenergieId,
|
||||
'EntladeenergieVariableID' => $batterieEntladeenergieId,
|
||||
'Energiefaktor' => 1.0,
|
||||
]];
|
||||
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
|
||||
$managerId = $test->createInstance(
|
||||
@@ -113,6 +123,8 @@ return static function (TestContext $test): void {
|
||||
'Manager',
|
||||
[
|
||||
'NetzleistungVariableID' => $netzleistungId,
|
||||
'NetzbezugEnergieVariableID' => $netzbezugEnergieId,
|
||||
'NetzeinspeisungEnergieVariableID' => $netzeinspeisungEnergieId,
|
||||
'EnergyPieAnzeigen' => true,
|
||||
'EnergiediagrammeAnzeigen' => true,
|
||||
'FunFactsAnzeigen' => true,
|
||||
@@ -226,11 +238,17 @@ return static function (TestContext $test): void {
|
||||
});
|
||||
$test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function (
|
||||
TestContext $test
|
||||
) use ($managerId): void {
|
||||
) use ($managerId, $pvLeistungId): void {
|
||||
IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true);
|
||||
$test->assertSame(
|
||||
4200.0,
|
||||
GetValue($test->objectByIdent('PVLeistungArchiv', $managerId))
|
||||
GetValue($test->objectByIdent('PVLeistungArchiv', $managerId)),
|
||||
sprintf(
|
||||
'PV-Archiv unveraendert; Quelle %.1f W, Managerstatus %d, Konfiguration %s.',
|
||||
(float) GetValue($pvLeistungId),
|
||||
(int) IPS_GetInstance($managerId)['InstanceStatus'],
|
||||
(string) IPS_GetProperty($managerId, 'AnlagenWechselrichter')
|
||||
)
|
||||
);
|
||||
$test->assertSame(
|
||||
2700.0,
|
||||
@@ -288,18 +306,69 @@ return static function (TestContext $test): void {
|
||||
}
|
||||
}
|
||||
$pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId);
|
||||
$test->assertSame(4200.0, (float) GetValue($pvFlussID));
|
||||
$test->assertSame(
|
||||
4200.0,
|
||||
(float) GetValue($pvFlussID),
|
||||
'Der anfaengliche PV-Energiefluss ist falsch.'
|
||||
);
|
||||
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||
|
||||
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1);
|
||||
IPS_ApplyChanges($managerId);
|
||||
$test->assertSame(4.2, (float) GetValue($pvFlussID));
|
||||
$test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||
$pieID = $test->objectByIdent('EnergyPie', $managerId);
|
||||
$leistungsdiagrammID = $test->objectByIdent('Leistungsdiagramm', $managerId);
|
||||
$energiediagrammID = $test->objectByIdent('Energiediagramm', $managerId);
|
||||
$energiediagramm = json_decode(
|
||||
base64_decode((string) IPS_GetMediaContent($energiediagrammID), true),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
$energietitel = array_column($energiediagramm['datasets'] ?? [], 'title');
|
||||
foreach ($energiediagramm['datasets'] ?? [] as $datensatz) {
|
||||
$test->assertSame('bar', $datensatz['type'] ?? null);
|
||||
}
|
||||
foreach ([
|
||||
'Netzbezug',
|
||||
'Netzeinspeisung',
|
||||
'Batterie laden gesamt',
|
||||
'Batterie entladen gesamt',
|
||||
] as $titel) {
|
||||
$test->assertTrue(in_array($titel, $energietitel, true));
|
||||
}
|
||||
|
||||
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
|
||||
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', true);
|
||||
$test->assertSame(false, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||
$angepasstesDiagramm = base64_encode(
|
||||
json_encode(['datasets' => [], 'custom' => 'bleibt'], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
IPS_SetMediaContent($energiediagrammID, $angepasstesDiagramm);
|
||||
IPS_SetProperty($pieID, 'Zaehlerfaktor', 2.0);
|
||||
IPS_ApplyChanges($pieID);
|
||||
IPS_SetProperty($energieflussID, 'Config', '[]');
|
||||
IPS_ApplyChanges($energieflussID);
|
||||
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1);
|
||||
IPS_SetProperty($managerId, 'DiagrammPVModus', 2);
|
||||
IPS_SetProperty($managerId, 'DiagrammBatterieModus', 2);
|
||||
IPS_ApplyChanges($managerId);
|
||||
|
||||
$test->assertSame($pieID, $test->objectByIdent('EnergyPie', $managerId));
|
||||
$test->assertSame(
|
||||
$leistungsdiagrammID,
|
||||
$test->objectByIdent('Leistungsdiagramm', $managerId)
|
||||
);
|
||||
$test->assertSame(
|
||||
$energiediagrammID,
|
||||
$test->objectByIdent('Energiediagramm', $managerId)
|
||||
);
|
||||
$test->assertSame(
|
||||
$angepasstesDiagramm,
|
||||
(string) IPS_GetMediaContent($energiediagrammID)
|
||||
);
|
||||
$test->assertSame(2.0, (float) IPS_GetProperty($pieID, 'Zaehlerfaktor'));
|
||||
$test->assertSame('[]', (string) IPS_GetProperty($energieflussID, 'Config'));
|
||||
$test->assertSame(
|
||||
4200.0,
|
||||
(float) GetValue($pvFlussID),
|
||||
'Der bestehende PV-Energiefluss wurde beim Apply veraendert.'
|
||||
);
|
||||
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||
|
||||
foreach ([
|
||||
'EnergyPieAnzeigen',
|
||||
@@ -322,7 +391,44 @@ return static function (TestContext $test): void {
|
||||
$ident . ' wurde beim Ausschalten nicht entfernt.'
|
||||
);
|
||||
}
|
||||
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||
$test->assertSame(
|
||||
true,
|
||||
(bool) IPS_GetObject($energieflussID)['ObjectIsHidden']
|
||||
);
|
||||
|
||||
foreach ([
|
||||
'EnergyPieAnzeigen',
|
||||
'EnergiediagrammeAnzeigen',
|
||||
'EnergieflussAnzeigen',
|
||||
] as $property) {
|
||||
IPS_SetProperty($managerId, $property, true);
|
||||
}
|
||||
IPS_ApplyChanges($managerId);
|
||||
$neuerFlussID = $test->objectByIdent('Energiefluss', $managerId);
|
||||
$test->assertSame($energieflussID, $neuerFlussID);
|
||||
$test->assertSame(false, (bool) IPS_GetObject($neuerFlussID)['ObjectIsHidden']);
|
||||
$test->assertTrue((string) IPS_GetProperty($neuerFlussID, 'Config') !== '[]');
|
||||
$test->assertSame(4.2, (float) GetValue($pvFlussID));
|
||||
$test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||
|
||||
$neuesLeistungsdiagramm = json_decode(
|
||||
base64_decode(
|
||||
(string) IPS_GetMediaContent(
|
||||
$test->objectByIdent('Leistungsdiagramm', $managerId)
|
||||
),
|
||||
true
|
||||
),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
$leistungstitel = array_column(
|
||||
$neuesLeistungsdiagramm['datasets'] ?? [],
|
||||
'title'
|
||||
);
|
||||
foreach (['PV gesamt', 'Hybridwechselrichter', 'Batterie gesamt', 'Hausspeicher'] as $titel) {
|
||||
$test->assertTrue(in_array($titel, $leistungstitel, true));
|
||||
}
|
||||
});
|
||||
|
||||
$zustandswert = static function (array $daten, int $instanzId, string $kennung) {
|
||||
@@ -470,12 +576,11 @@ return static function (TestContext $test): void {
|
||||
));
|
||||
});
|
||||
|
||||
$test->runCase('Veralteter Verbraucher blockiert aktuelle Verbraucher nicht', static function (
|
||||
$test->runCase('Veraltete Verbraucher werden aktiv neu angefordert', static function (
|
||||
TestContext $test
|
||||
) use (
|
||||
$managerId,
|
||||
$netzleistungId,
|
||||
$einStufigId,
|
||||
$warmwasserId,
|
||||
$pufferspeicherId,
|
||||
$melden,
|
||||
@@ -496,17 +601,9 @@ return static function (TestContext $test): void {
|
||||
IPS_RequestAction($managerId, 'Regeln', true);
|
||||
|
||||
$daten = $verbraucherstatus();
|
||||
$test->assertSame(false, $daten['Synchronisiert'] ?? null);
|
||||
$test->assertSame(2, count($daten['Verbraucher'] ?? []));
|
||||
$test->assertSame(1, count($daten['NichtGeregelt'] ?? []));
|
||||
$test->assertSame(
|
||||
$einStufigId,
|
||||
$daten['NichtGeregelt'][0]['InstanzID'] ?? null
|
||||
);
|
||||
$test->assertSame(
|
||||
'Meldung ist veraltet',
|
||||
$daten['NichtGeregelt'][0]['Grund'] ?? null
|
||||
);
|
||||
$test->assertSame(true, $daten['Synchronisiert'] ?? null);
|
||||
$test->assertSame(3, count($daten['Verbraucher'] ?? []));
|
||||
$test->assertSame(0, count($daten['NichtGeregelt'] ?? []));
|
||||
$test->assertTrue(
|
||||
(float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user