30 Commits
Author SHA1 Message Date
dh 5437f3f058 fix(verbraucher): clear discarded power setpoints
Tests / test (push) Failing after 51s
2026-10-06 13:45:54 +00:00
dh ebec355c96 feat(manager): account for SDL power and SOC separately
Tests / test (push) Failing after 1m12s
Add opt-in SDL measurement sources and independent visualization toggles. Exclude balancing power from household telemetry and new household energy. Preserve custom views and cover measurement, migration and Symcon 8.0 behavior.
2026-10-06 13:35:17 +00:00
dh 58f91e4319 feat(manager): distribute equal-priority power stepwise
Tests / test (push) Successful in 44s
2026-10-06 11:55:26 +00:00
dh ec7182b833 fix(charger): stabilize Go-E setpoint control
Tests / test (push) Successful in 47s
2026-10-06 11:33:59 +00:00
dh 37addf4242 feat(manager): Energievisualisierung und Synchronisierung erweitern
Tests / test (push) Successful in 50s
2026-10-06 08:00:30 +00:00
dh 8d83b083bf feat(batterie): Nachladeleistung begrenzen
Tests / test (push) Successful in 48s
2026-10-01 15:56:23 +00:00
dh 6cd7bf19f5 test(ladestation): Regeluebergang abwarten 2026-10-01 15:56:14 +00:00
dh 3ce9af4244 fix(ladestation): Regeluebergaenge stabilisieren
Tests / test (push) Successful in 50s
2026-10-01 13:01:32 +00:00
dh 8357e8dfab merge: vorbereiteten Fahrplan-Commit uebernehmen
Tests / test (push) Successful in 54s
2026-10-01 12:34:51 +00:00
dh 38353aed93 feat(manager): Fahrplanvorgabe linear interpolieren
Tests / test (push) Successful in 54s
2026-10-01 12:16:10 +00:00
dh cf92e7623b fix(manager): Fahrplanvorgabe linear interpolieren 2026-09-30 14:38:42 +02:00
dh add1077674 fix(manager): Batterie nach Prioritaet als Restverbraucher regeln
Tests / test (push) Canceled after 0s
2026-09-30 05:42:13 +00:00
dh bfdcf4536b fix(ladestation): Steuerbefehle in Diagnose erhalten
Tests / test (push) Canceled after 0s
2026-09-30 04:37:33 +00:00
dh faa5dfb33f feat(ladestation): Phasenerkennung und Schaltsperren erweitern
Tests / test (push) Successful in 45s
2026-09-29 22:49:26 +00:00
dh 53fca5e2bb feat(manager): Batteriesollwert aus Netzfahrplan anwenden
Tests / test (push) Successful in 47s
2026-09-29 17:23:25 +00:00
dh f611da975d fix(easee): Ereignisverbindung konsistent neu aufbauen
Tests / test (push) Successful in 1m2s
2026-09-29 16:47:03 +00:00
dh 393969594e fix(manager): Regelung und Energiefluss korrigieren
Tests / test (push) Successful in 48s
2026-09-29 16:17:49 +00:00
dh cae14cf399 fix(manager): Festlasten nach Istleistung bilanzieren
Tests / test (push) Successful in 52s
2026-09-29 15:11:14 +00:00
dh 3aaa744a8a fix(manager): Batterie als neutralen Restregler behandeln
Tests / test (push) Successful in 51s
2026-09-29 14:38:14 +00:00
dh fdc2007bb1 fix(manager): Energieausgleich aus Enelix 1 wiederherstellen
Tests / test (push) Successful in 54s
2026-09-29 14:14:25 +00:00
dh 37400878b6 fix(easee): veraltete Ladeleistung nach Ladestopp nullen
Tests / test (push) Successful in 50s
2026-09-29 13:15:43 +00:00
dh ace1821213 fix(ladestation): Fahrzeugstrom nach Lernzeit begrenzen
Tests / test (push) Successful in 56s
2026-09-29 12:47:23 +00:00
dh b3b868cd2a fix(ladestation): Ladeanlauf und Easee-Verbindung stabilisieren
Tests / test (push) Successful in 1m23s
2026-09-29 12:25:35 +00:00
dh a74f14aa19 feat(warmwasser): Temperaturgrenzen konfigurierbar machen
Tests / test (push) Successful in 1m10s
2026-09-29 12:11:43 +00:00
dh fe966516b5 test: integrierte Freigabepruefungen stabilisieren
Tests / test (push) Successful in 1m14s
2026-09-29 12:06:51 +00:00
dh b3bbae1a72 fix(manager): Anlagentopologie immer synchronisieren
Tests / test (push) Failing after 50s
2026-09-29 11:51:55 +00:00
dh ae2cd8ecff Merge remote-tracking branch 'origin/develop' into develop
Tests / test (push) Successful in 46s
2026-09-29 11:43:12 +00:00
dh e0814a0c14 fix(ladestation): Ladeende nach Update neu prüfen 2026-09-29 09:19:39 +00:00
dh b86810d30f fix(ladestation): Solarpause nicht als Ladeende erkennen 2026-09-29 09:07:44 +00:00
dh 21577c0125 fix(manager): Prognoseberechtigungen verständlich diagnostizieren 2026-09-29 08:41:32 +00:00
54 changed files with 5347 additions and 490 deletions
+11 -4
View File
@@ -82,19 +82,26 @@ Messwert, ist er veraltet oder liegt der Ladezustand ausserhalb von 0 bis
ReserveLadezustand entspricht der bisherigen Peakshaving-Reserve. ReserveLadezustand entspricht der bisherigen Peakshaving-Reserve.
MindestLadezustand schuetzt vor weiterer Entladung. MindestLadezustand schuetzt vor weiterer Entladung.
LadezustandHysterese ersetzt die bisher fest im Code hinterlegte 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: Die bisherigen betriebsartabhaengigen Angebote bleiben erhalten:
- Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der - Im PV-Betrieb steht oberhalb der Reserve der durchgaengige Bereich von der
maximalen Entlade- bis zur maximalen Ladeleistung zur Verfuegung. 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 - Bei vollem Speicher wird der durchgaengige Bereich von der maximalen
Entladeleistung bis 0 W angeboten. Entladeleistung bis 0 W angeboten.
- Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller - Im Peakbetrieb wird oberhalb der Reserve der aus Netz- und aktueller
Batterieleistung berechnete Entladewert angeboten. Batterieleistung berechnete Entladewert angeboten.
- Innerhalb der Reserve-Hysterese und unterhalb des Mindestladezustands werden - 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 ## Manager und Zeitverhalten
@@ -133,7 +140,7 @@ verwirft die aktuelle Vorgabe und schreibt eine Leistung von 0.
1. Batterietyp und Steuerung auswaehlen. 1. Batterietyp und Steuerung auswaehlen.
2. Alle fuenf Messwertvariablen zuordnen. 2. Alle fuenf Messwertvariablen zuordnen.
3. Die automatisch eingeblendeten Registervariablen 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. 5. Diagnosevariablen und Logging fuer die Erstinbetriebnahme aktivieren.
6. Die Batterie im Manager zuordnen. 6. Die Batterie im Manager zuordnen.
7. Aktiv einschalten. 7. Aktiv einschalten.
+14
View File
@@ -137,6 +137,20 @@
"minimum": 0, "minimum": 0,
"maximum": 100, "maximum": 100,
"digits": 1 "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
} }
] ]
}, },
+54 -8
View File
@@ -86,6 +86,8 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterPropertyFloat('ReserveLadezustand', 20.0); $this->RegisterPropertyFloat('ReserveLadezustand', 20.0);
$this->RegisterPropertyFloat('MindestLadezustand', 10.0); $this->RegisterPropertyFloat('MindestLadezustand', 10.0);
$this->RegisterPropertyFloat('LadezustandHysterese', 2.0); $this->RegisterPropertyFloat('LadezustandHysterese', 2.0);
$this->RegisterPropertyBoolean('NachladenMitMaximalleistung', true);
$this->RegisterPropertyInteger('MaximaleNachladeleistung', 0);
$this->RegisterPropertyInteger('MesswertMaxAlter', 60); $this->RegisterPropertyInteger('MesswertMaxAlter', 60);
$this->RegisterPropertyInteger('Aenderungssperre', 4); $this->RegisterPropertyInteger('Aenderungssperre', 4);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false); $this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
@@ -151,6 +153,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMesswertmeldungen(); $this->registriereMesswertmeldungen();
$this->WriteAttributeBoolean('RegisterInitialisiert', false); $this->WriteAttributeBoolean('RegisterInitialisiert', false);
@@ -185,6 +188,11 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$formular['elements'], $formular['elements'],
$this->ReadPropertyInteger('Batterietyp') $this->ReadPropertyInteger('Batterietyp')
); );
$this->setzeFormfeldSichtbar(
$formular['elements'],
'MaximaleNachladeleistung',
!$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
);
return json_encode($formular, JSON_THROW_ON_ERROR); return json_encode($formular, JSON_THROW_ON_ERROR);
} }
@@ -212,7 +220,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -228,7 +236,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -242,7 +250,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
return; return;
case 'SichererZustand': case 'SichererZustand':
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->schreibeRegister(0, true); $this->schreibeRegister(0, true);
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -251,6 +259,14 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->aktualisiereFormRegisterfelder((int) $wert); $this->aktualisiereFormRegisterfelder((int) $wert);
return; return;
case 'FormNachladenMitMaximalleistung':
$this->UpdateFormField(
'MaximaleNachladeleistung',
'visible',
!(bool) $wert
);
return;
case 'ManagerdatenEmpfangen': case 'ManagerdatenEmpfangen':
if (!is_string($wert)) { if (!is_string($wert)) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
@@ -298,7 +314,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
(int) $this->leseZustand('Sollleistung') (int) $this->leseZustand('Sollleistung')
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false); $this->aktualisiere(false);
} }
$this->planeMeldung(); $this->planeMeldung();
@@ -382,7 +398,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$messwerte['Netzleistung'], $messwerte['Netzleistung'],
$messwerte['Istleistung'], $messwerte['Istleistung'],
$messwerte['MaxLaden'], $messwerte['MaxLaden'],
$messwerte['MaxEntladen'] $messwerte['MaxEntladen'],
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
? null
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
) )
: [0]; : [0];
@@ -401,7 +420,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
(int) $this->leseZustand('Sollleistung') (int) $this->leseZustand('Sollleistung')
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->pruefeVorgabeTimeout($jetzt); $this->pruefeVorgabeTimeout($jetzt);
@@ -578,7 +597,7 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
@@ -776,6 +795,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( BatterieRegler::hystereseAktiv(
50.0, 50.0,
$this->ReadPropertyFloat('ReserveLadezustand'), $this->ReadPropertyFloat('ReserveLadezustand'),
@@ -793,7 +820,10 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
0.0, 0.0,
0.0, 0.0,
1.0, 1.0,
1.0 1.0,
$this->ReadPropertyBoolean('NachladenMitMaximalleistung')
? null
: (float) $this->ReadPropertyInteger('MaximaleNachladeleistung')
); );
foreach (self::MESSWERT_PROPERTIES as $property) { foreach (self::MESSWERT_PROPERTIES as $property) {
@@ -1096,6 +1126,22 @@ class Batterie extends IPSModule implements VerbraucherSchnittstelle
$this->setzeZustand('Stoertext', $text); $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 */ /** @param array<int, mixed> $elemente */
private function setzeRegisterfelderSichtbar( private function setzeRegisterfelderSichtbar(
array &$elemente, array &$elemente,
+28
View File
@@ -2,6 +2,28 @@
Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert. Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
## Unreleased
- Verbraucher: Beim Verwerfen einer Manager-Vorgabe wird auch die gespeicherte
und sichtbare Sollleistung auf 0 W gesetzt. Gilt fuer Batterie, beide
Ladestationen, Pufferspeicher, Verbraucher 1-Stufig, Waermepumpe und
Warmwassererwaermer. Bereits ungueltige Altwerte werden beim Anwenden der
Instanzkonfiguration bereinigt. Gueltige Vorgaben, lokale Schutzprogramme,
Mindestlaufzeiten und der Nachrichtenvertrag bleiben erhalten.
- Manager: SDL-/Regelenergie als separate Istleistungsquelle mit optionalem SOC,
Leistungsfaktor und getrennten Anzeigeoptionen fuer Energiefluss und Diagramme.
SDL wird unabhaengig von der Anzeige aus Hauslast und neuer Hausenergie
herausgerechnet; die Prognosetelemetrie verwendet die korrigierte Hauslast.
Bestehende Visualisierungen werden ausschliesslich um eigene SDL-Eintraege
ergaenzt. Standardmaessig deaktiviert, keine automatische Quellenmigration,
keine rueckwirkende Datenkorrektur und keine Aenderung externer SDL-Steuerungen.
- 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 ## 0.1 Build 1 - Erste Beta - 2026-09-29
### Enthalten ### Enthalten
@@ -15,12 +37,18 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
### Beta-Aenderungen ### 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. - 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. - 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. - Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
- Wartende Easee-Ladestationen erhalten bei positiver Stromvorgabe einmal pro Fahrzeugverbindung einen Startbefehl; Freigabe- und Blockiergruende werden diagnostiziert. - Wartende Easee-Ladestationen erhalten bei positiver Stromvorgabe einmal pro Fahrzeugverbindung einen Startbefehl; Freigabe- und Blockiergruende werden diagnostiziert.
- Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr. - Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr.
- Verbraucher gleicher Prioritaet werden wie in Enelix 1 anhand ihrer bezogenen Energie in 2-kWh-Gruppen ausgeglichen.
- Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an. - Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an.
- Der Warmwassererwaermer verwendet konfigurierbare Temperatur-Stoergrenzen von standardmaessig 0 bis 100 Grad C.
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module. - Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
### Hinweise ### Hinweise
+105 -20
View File
@@ -16,6 +16,7 @@ class EaseeGateway extends IPSModule
private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers'; private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers';
private const API_BASE_URL = 'https://api.easee.com'; private const API_BASE_URL = 'https://api.easee.com';
private const RECORD_SEPARATOR = "\x1e"; private const RECORD_SEPARATOR = "\x1e";
private const CACHE_MAXIMALALTER_SEKUNDEN = 60;
public function Create(): void public function Create(): void
{ {
@@ -50,6 +51,11 @@ class EaseeGateway extends IPSModule
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);" "IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);"
); );
$this->RegisterTimer(
'ReconnectTimer',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'ReconnectAfterLoss', false);"
);
$this->RequireParent(self::WEBSOCKET_MODULE_ID); $this->RequireParent(self::WEBSOCKET_MODULE_ID);
} }
@@ -60,6 +66,7 @@ class EaseeGateway extends IPSModule
$this->SetTimerInterval('MaintainConnectionTimer', 0); $this->SetTimerInterval('MaintainConnectionTimer', 0);
$this->SetTimerInterval('TokenRefreshTimer', 0); $this->SetTimerInterval('TokenRefreshTimer', 0);
$this->SetTimerInterval('ReconnectTimer', 0);
$this->SetBuffer('SignalRReady', '0'); $this->SetBuffer('SignalRReady', '0');
$this->SetBuffer('ReceiveBuffer', ''); $this->SetBuffer('ReceiveBuffer', '');
$this->SetBuffer('Subscriptions', '{}'); $this->SetBuffer('Subscriptions', '{}');
@@ -111,8 +118,22 @@ class EaseeGateway extends IPSModule
$this->pflegeVerbindung(); $this->pflegeVerbindung();
return; return;
case 'ReconnectAfterLoss':
$this->SetTimerInterval('ReconnectTimer', 0);
if (
$this->ReadPropertyBoolean('Active')
&& !$this->ReadPropertyBoolean('Testmodus')
&& $this->GetBuffer('SignalRReady') !== '1'
) {
$this->verbindeSignalR();
}
return;
case 'RefreshToken': case 'RefreshToken':
if ($this->erneuereZugangsdaten(false)) { if (
$this->erneuereZugangsdaten(false)
&& $this->GetBuffer('SignalRReady') !== '1'
) {
$this->verbindeSignalR(); $this->verbindeSignalR();
} }
return; return;
@@ -142,6 +163,15 @@ class EaseeGateway extends IPSModule
$daten['value'] ?? null $daten['value'] ?? null
); );
return; return;
case 'TestSignalRFrame':
if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) {
throw new InvalidArgumentException(
'TestSignalRFrame ist nur im Testmodus zulaessig.'
);
}
$this->verarbeiteSignalRRahmen($wert);
return;
} }
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident); throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
@@ -277,8 +307,10 @@ class EaseeGateway extends IPSModule
if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) { if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) {
return; return;
} }
$this->SetBuffer('SignalRReady', '0'); $this->planeWiederverbindung(
$this->setzeVerbunden(false); 'Seit mehr als 90 Sekunden keine Easee-Ereignisdaten empfangen.'
);
return;
} }
if (($jetzt - $letzteAushandlung) >= 45) { if (($jetzt - $letzteAushandlung) >= 45) {
@@ -291,16 +323,22 @@ class EaseeGateway extends IPSModule
private function verbindeSignalR(): bool private function verbindeSignalR(): bool
{ {
$this->SetTimerInterval('ReconnectTimer', 0);
if ($this->ReadPropertyBoolean('Testmodus')) { if ($this->ReadPropertyBoolean('Testmodus')) {
$this->SetStatus(102); $this->setzeEreignisverbindung(true);
$this->setzeVerbunden(true);
return true; return true;
} }
if (!$this->stelleAccessTokenSicher()) { if (!$this->stelleAccessTokenSicher()) {
$this->SetBuffer('SignalRReady', '0');
$this->setzeVerbunden(false);
$this->SetStatus(202); $this->SetStatus(202);
return false; return false;
} }
$this->setzeEreignisverbindung(
false,
'Easee-Ereignisverbindung wird aufgebaut.'
);
$this->SetBuffer('LastNegotiation', (string) time()); $this->SetBuffer('LastNegotiation', (string) time());
$token = $this->GetBuffer('AccessToken'); $token = $this->GetBuffer('AccessToken');
$antwort = $this->httpAnfrage( $antwort = $this->httpAnfrage(
@@ -319,8 +357,8 @@ class EaseeGateway extends IPSModule
); );
} }
if (!$antwort['success']) { if (!$antwort['success']) {
$this->SetStatus(203); $this->setzeEreignisverbindung(
$this->setzeLetztenFehler( false,
'Easee-Ereignisverbindung fehlgeschlagen: HTTP ' 'Easee-Ereignisverbindung fehlgeschlagen: HTTP '
. $antwort['httpCode'] . $antwort['httpCode']
. ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '') . ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '')
@@ -333,8 +371,10 @@ class EaseeGateway extends IPSModule
? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '') ? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '')
: ''; : '';
if ($verbindungstoken === '') { if ($verbindungstoken === '') {
$this->SetStatus(203); $this->setzeEreignisverbindung(
$this->setzeLetztenFehler('Easee liefert kein Verbindungstoken.'); false,
'Easee liefert kein Verbindungstoken.'
);
return false; return false;
} }
@@ -346,7 +386,7 @@ class EaseeGateway extends IPSModule
$this->SetBuffer('ReceiveBuffer', ''); $this->SetBuffer('ReceiveBuffer', '');
$this->SetBuffer('SubscribedThisConnection', '{}'); $this->SetBuffer('SubscribedThisConnection', '{}');
$this->SetBuffer('PendingInvocations', '{}'); $this->SetBuffer('PendingInvocations', '{}');
$this->WriteAttributeString('ObservationCache', '{}'); $this->markiereBeobachtungscacheVeraltet();
$this->setzeVerbunden(false); $this->setzeVerbunden(false);
if (!$this->konfiguriereWebSocketParent($url)) { if (!$this->konfiguriereWebSocketParent($url)) {
@@ -409,10 +449,7 @@ class EaseeGateway extends IPSModule
private function verarbeiteSignalRRahmen(string $rahmen): void private function verarbeiteSignalRRahmen(string $rahmen): void
{ {
if ($rahmen === '{}') { if ($rahmen === '{}') {
$this->SetBuffer('SignalRReady', '1'); $this->setzeEreignisverbindung(true);
$this->SetStatus(102);
$this->setzeLetztenFehler('');
$this->setzeVerbunden(true);
$this->sendeAlleAbonnements(); $this->sendeAlleAbonnements();
return; return;
} }
@@ -423,8 +460,9 @@ class EaseeGateway extends IPSModule
return; return;
} }
if (isset($nachricht['error'])) { if (isset($nachricht['error'])) {
$this->SetStatus(203); $this->planeWiederverbindung(
$this->setzeLetztenFehler('Easee-Ereignisfehler: ' . $nachricht['error']); 'Easee-Ereignisfehler: ' . $nachricht['error']
);
return; return;
} }
@@ -433,9 +471,9 @@ class EaseeGateway extends IPSModule
return; return;
} }
if ($typ === 7) { if ($typ === 7) {
$this->SetBuffer('SignalRReady', '0'); $this->planeWiederverbindung(
$this->setzeVerbunden(false); 'Easee-Ereignisverbindung wurde beendet.'
$this->setzeLetztenFehler('Easee-Ereignisverbindung wurde beendet.'); );
return; return;
} }
@@ -572,12 +610,30 @@ class EaseeGateway extends IPSModule
); );
} }
private function markiereBeobachtungscacheVeraltet(): void
{
$cache = $this->leseAttributArray('ObservationCache');
foreach ($cache as &$zustand) {
if (is_array($zustand)) {
$zustand['updated'] = 0;
}
}
unset($zustand);
$this->WriteAttributeString(
'ObservationCache',
json_encode($cache, JSON_THROW_ON_ERROR)
);
}
private function stelleAktuellenStatusSicher(string $seriennummer): void private function stelleAktuellenStatusSicher(string $seriennummer): void
{ {
$cache = $this->leseAttributArray('ObservationCache'); $cache = $this->leseAttributArray('ObservationCache');
$zustand = $cache[$seriennummer] ?? []; $zustand = $cache[$seriennummer] ?? [];
$cacheIstAktuell = is_array($zustand)
&& array_key_exists('109', $zustand)
&& (int) ($zustand['updated'] ?? 0) >= time() - self::CACHE_MAXIMALALTER_SEKUNDEN;
if ( if (
(is_array($zustand) && array_key_exists('109', $zustand)) $cacheIstAktuell
|| $this->ReadPropertyBoolean('Testmodus') || $this->ReadPropertyBoolean('Testmodus')
|| $this->GetBuffer('SignalRReady') !== '1' || $this->GetBuffer('SignalRReady') !== '1'
) { ) {
@@ -846,9 +902,38 @@ class EaseeGateway extends IPSModule
]; ];
} }
private function setzeEreignisverbindung(
bool $verbunden,
string $fehler = ''
): void {
$this->SetBuffer('SignalRReady', $verbunden ? '1' : '0');
$this->SetStatus($verbunden ? 102 : 203);
$this->setzeLetztenFehler($verbunden ? '' : $fehler);
if ($verbunden) {
$this->SetTimerInterval('ReconnectTimer', 0);
}
$this->setzeVerbunden($verbunden);
}
private function planeWiederverbindung(string $fehler): void
{
$this->setzeEreignisverbindung(false, $fehler);
$this->SetBuffer('LastNegotiation', '0');
if (
$this->ReadPropertyBoolean('Active')
&& !$this->ReadPropertyBoolean('Testmodus')
) {
$this->SetTimerInterval('ReconnectTimer', 1000);
}
}
private function setzeVerbunden(bool $verbunden): void private function setzeVerbunden(bool $verbunden): void
{ {
$warVerbunden = (bool) $this->GetValue('Connected');
$this->SetValue('Connected', $verbunden); $this->SetValue('Connected', $verbunden);
if ($warVerbunden === $verbunden) {
return;
}
$this->SendDataToChildren(json_encode([ $this->SendDataToChildren(json_encode([
'DataID' => self::CHILD_EVENT_DATA_ID, 'DataID' => self::CHILD_EVENT_DATA_ID,
'Buffer' => json_encode([ 'Buffer' => json_encode([
+120 -59
View File
@@ -28,17 +28,17 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private const STATUS_GATEWAY_GETRENNT = 202; private const STATUS_GATEWAY_GETRENNT = 202;
private const STATUS_WARTET_AUF_DATEN = 203; private const STATUS_WARTET_AUF_DATEN = 203;
private const STATUS_LADEFEHLER = 204; private const STATUS_LADEFEHLER = 204;
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
/** @var list<string> */ /** @var list<string> */
private const DIAGNOSE_VARIABLEN = [ private const DIAGNOSE_VARIABLEN = [
'Istleistung',
'Leistungsquelle', 'Leistungsquelle',
'Sollleistung',
'SollwertGueltig', 'SollwertGueltig',
'Verfuegbar', 'Verfuegbar',
'AenderungMoeglich', 'AenderungMoeglich',
'Stoerung', 'Stoerung',
'Stoertext', 'Stoertext',
'Fahrzeugstatus',
'GatewayVerbunden', 'GatewayVerbunden',
'ApiMaximalstrom', 'ApiMaximalstrom',
'EaseeAktiviert', 'EaseeAktiviert',
@@ -75,7 +75,6 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'~Switch', '~Switch',
110 110
); );
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130); $this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140); $this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
@@ -113,6 +112,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false); $this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->RegisterAttributeString('LetzterGeraetebefehl', ''); $this->RegisterAttributeString('LetzterGeraetebefehl', '');
$this->RegisterAttributeString('LetzterStartbefehl', ''); $this->RegisterAttributeString('LetzterStartbefehl', '');
$this->RegisterAttributeBoolean('RegeluebergangAktiv', false);
$this->RegisterTimer( $this->RegisterTimer(
'Meldezyklus', 'Meldezyklus',
@@ -129,6 +129,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);" "IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
); );
$this->RegisterTimer(
'RegeluebergangTimeout',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
);
$this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID); $this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID);
} }
@@ -139,9 +144,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->initialisiereLokaleEinstellungen(); $this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->WriteAttributeBoolean('GatewayVerbunden', false); $this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false); $this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->beendeRegeluebergang();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -167,17 +174,27 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
{ {
switch ($ident) { switch ($ident) {
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $aktiv = (bool) $wert;
if (!(bool) $wert) { $this->SetValue('Aktiv', $aktiv);
$this->setzeZustand('SollwertGueltig', false); if ($aktiv) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
case 'Ladefreigabe': case 'Ladefreigabe':
$this->WriteAttributeBoolean('ZustandLadefreigabe', (bool) $wert); $ladefreigabe = (bool) $wert;
$this->setzeSichtbareVariable('Ladefreigabe', (bool) $wert); $this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
$this->setzeZustand('SollwertGueltig', false); $this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
if ($ladefreigabe) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->verwerfeSollwert();
}
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -185,9 +202,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$solarladen = EaseeLadestatus::solarladenErzwungen( $solarladen = EaseeLadestatus::solarladenErzwungen(
$this->ReadPropertyInteger('Betriebsmodus') $this->ReadPropertyInteger('Betriebsmodus')
) ? true : (bool) $wert; ) ? true : (bool) $wert;
if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
$this->starteRegeluebergang();
}
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen); $this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
$this->setzeSichtbareVariable('Solarladen', $solarladen); $this->setzeSichtbareVariable('Solarladen', $solarladen);
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -197,7 +216,12 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true);
return;
case 'RegeluebergangTimeout':
$this->beendeRegeluebergang();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -288,7 +312,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
} }
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']); $this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
$this->aktualisiere(false); $this->aktualisiere(false, false);
$sollleistung = $daten['Sollleistung_W']; $sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) { if ($sollleistung === null) {
if ( if (
@@ -299,7 +323,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
true true
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false); $this->aktualisiere(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -311,6 +335,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
); );
} }
$this->beendeRegeluebergang();
$this->WriteAttributeInteger('LetzteManagerID', $managerID); $this->WriteAttributeInteger('LetzteManagerID', $managerID);
$this->WriteAttributeInteger('LetzteVorgabeZeit', time()); $this->WriteAttributeInteger('LetzteVorgabeZeit', time());
$this->setzeZustand('Sollleistung', $sollleistung); $this->setzeZustand('Sollleistung', $sollleistung);
@@ -405,7 +430,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeInteger('LetzteBeobachtung', $zeitpunkt); $this->WriteAttributeInteger('LetzteBeobachtung', $zeitpunkt);
} }
private function aktualisiere(bool $meldungPlanen): void private function aktualisiere(
bool $meldungPlanen,
bool $steuerungAusfuehren = true
): void
{ {
try { try {
$status = EaseeLadestatus::ausBeobachtungen( $status = EaseeLadestatus::ausBeobachtungen(
@@ -493,12 +521,21 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
} }
$this->setzeStoerung($stoertext); $this->setzeStoerung($stoertext);
$regeluebergangAktiv = (bool) $this->GetValue('Aktiv')
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
&& $steuerbar
&& $status['FahrzeugVerbunden']
&& !$status['FahrzeugGeladen']
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
if ($gatewayVerbunden && $status['StatusGueltig']) { if ($gatewayVerbunden && $status['StatusGueltig']) {
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
if ($steuerungAusfuehren && !$regeluebergangAktiv) {
$this->setzeLadestrom( $this->setzeLadestrom(
$this->bestimmeWirksameSollleistung($angebot), $sollleistung,
$regelPhasenzahl, $regelPhasenzahl,
$status['Fahrzeugstatus'] === 2 $status['Fahrzeugstatus'] === 2
); );
}
} else { } else {
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
} }
@@ -523,7 +560,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($sollleistung, $angebot, true)) { if (in_array($sollleistung, $angebot, true)) {
return $sollleistung; return $sollleistung;
} }
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
if ( if (
@@ -797,6 +834,23 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
return is_array($zuordnung) ? $zuordnung : []; 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 private function pruefeKonfiguration(): void
{ {
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) { foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
@@ -857,95 +911,63 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private function aktualisiereVariablen(): void 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')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose(); $this->registriereVerbraucherDiagnose();
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'GatewayVerbunden', 'GatewayVerbunden',
'Easee Gateway verbunden', 'Easee Gateway verbunden',
'~Switch', '~Switch',
200
);
$this->RegisterVariableInteger(
'ApiMaximalstrom',
'API-Maximalstrom',
'',
210 210
); );
$this->RegisterVariableInteger('ApiMaximalstrom', 'API-Maximalstrom', '', 220);
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'EaseeAktiviert', 'EaseeAktiviert',
'Easee aktiviert', 'Easee aktiviert',
'~Switch', '~Switch',
220 230
); );
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'AutorisierungErforderlich', 'AutorisierungErforderlich',
'Autorisierung erforderlich', 'Autorisierung erforderlich',
'~Switch', '~Switch',
230 240
); );
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'RemoteStartErforderlich', 'RemoteStartErforderlich',
'Remote-Start erforderlich', 'Remote-Start erforderlich',
'~Switch', '~Switch',
240 250
); );
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'SmartCharging', 'SmartCharging',
'Easee Smart Charging', 'Easee Smart Charging',
'~Switch', '~Switch',
250 260
); );
$this->RegisterVariableInteger( $this->RegisterVariableInteger(
'GrundKeinLadestrom', 'GrundKeinLadestrom',
'Grund kein Ladestrom', 'Grund kein Ladestrom',
'', '',
260 270
); );
$this->RegisterVariableString( $this->RegisterVariableString(
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
'Letzter Geraetebefehl', 'Letzter Geraetebefehl',
'', '',
270 280
); );
$this->RegisterVariableString( $this->RegisterVariableString(
'LetzterStartbefehl', 'LetzterStartbefehl',
'Letzter Startbefehl', 'Letzter Startbefehl',
'', '',
280 290
); );
$this->RegisterVariableString( $this->RegisterVariableString(
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
'Leistungsangebot', 'Leistungsangebot',
'', '',
290 300
); );
$this->SetValue( $this->SetValue(
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
@@ -983,6 +1005,40 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->entferneVariable($ident); $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 private function entferneVariable(string $ident): void
@@ -1108,7 +1164,12 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private function deaktiviereTimer(): void private function deaktiviereTimer(): void
{ {
foreach (['Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) { foreach ([
'Meldezyklus',
'RueckmeldungVerzoegert',
'VorgabeTimeout',
'RegeluebergangTimeout',
] as $timer) {
$this->SetTimerInterval($timer, 0); $this->SetTimerInterval($timer, 0);
} }
} }
+35
View File
@@ -37,6 +37,41 @@
"caption": "Geraetestatus abfragen", "caption": "Geraetestatus abfragen",
"suffix": " s", "suffix": " s",
"minimum": 1 "minimum": 1
},
{
"type": "NumberSpinner",
"name": "Phasenerkennungszeit",
"caption": "Phasen erkennen nach",
"suffix": " s",
"minimum": 1
},
{
"type": "NumberSpinner",
"name": "FahrzeugstromErkennungszeit",
"caption": "Fahrzeug-Maximalstrom erkennen nach",
"suffix": " s",
"minimum": 30
},
{
"type": "NumberSpinner",
"name": "ZeitZwischenZustandswechseln",
"caption": "Zeit zwischen Leistungsaenderungen",
"suffix": " min",
"minimum": 0
},
{
"type": "NumberSpinner",
"name": "Mindesteinschaltdauer",
"caption": "Mindesteinschaltdauer",
"suffix": " min",
"minimum": 0
},
{
"type": "NumberSpinner",
"name": "Mindestausschaltdauer",
"caption": "Mindestausschaltdauer",
"suffix": " min",
"minimum": 0
} }
] ]
}, },
+398 -38
View File
@@ -23,17 +23,19 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private const STATUS_AKTIV = 102; private const STATUS_AKTIV = 102;
private const STATUS_KONFIGURATION_UNGUELTIG = 201; private const STATUS_KONFIGURATION_UNGUELTIG = 201;
private const STATUS_KOMMUNIKATIONSFEHLER = 202; private const STATUS_KOMMUNIKATIONSFEHLER = 202;
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
private const PHASENERKENNUNGSSTROM = 6;
/** @var list<string> */ /** @var list<string> */
private const DIAGNOSE_VARIABLEN = [ private const DIAGNOSE_VARIABLEN = [
'Istleistung',
'Leistungsquelle', 'Leistungsquelle',
'Sollleistung',
'SollwertGueltig', 'SollwertGueltig',
'Verfuegbar', 'Verfuegbar',
'AenderungMoeglich', 'AenderungMoeglich',
'Stoerung', 'Stoerung',
'Stoertext', 'Stoertext',
'Fahrzeugstatus',
'FahrzeugMaximalstrom',
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
]; ];
@@ -50,6 +52,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterPropertyString('Benutzername', ''); $this->RegisterPropertyString('Benutzername', '');
$this->RegisterPropertyString('Passwort', ''); $this->RegisterPropertyString('Passwort', '');
$this->RegisterPropertyInteger('MaximalerLadestrom', 16); $this->RegisterPropertyInteger('MaximalerLadestrom', 16);
$this->RegisterPropertyInteger('Phasenerkennungszeit', 60);
$this->RegisterPropertyInteger('FahrzeugstromErkennungszeit', 90);
$this->RegisterPropertyInteger('ZeitZwischenZustandswechseln', 1);
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 0);
$this->RegisterPropertyInteger('Mindestausschaltdauer', 0);
$this->RegisterPropertyInteger('Abfrageintervall', 5); $this->RegisterPropertyInteger('Abfrageintervall', 5);
$this->RegisterPropertyBoolean('Ladefreigabe', true); $this->RegisterPropertyBoolean('Ladefreigabe', true);
$this->RegisterPropertyBoolean('Solarladen', true); $this->RegisterPropertyBoolean('Solarladen', true);
@@ -61,7 +68,6 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterVariableBoolean('FahrzeugVerbunden', 'Fahrzeug verbunden', '~Switch', 100); $this->RegisterVariableBoolean('FahrzeugVerbunden', 'Fahrzeug verbunden', '~Switch', 100);
$this->RegisterVariableBoolean('FahrzeugGeladen', 'Fahrzeug geladen', '~Switch', 110); $this->RegisterVariableBoolean('FahrzeugGeladen', 'Fahrzeug geladen', '~Switch', 110);
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130); $this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140); $this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
@@ -87,6 +93,18 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1); $this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
$this->RegisterAttributeInteger('LetzteGesetzteLeistung', 0);
$this->RegisterAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->RegisterAttributeInteger('AusschaltenGesperrtBis', 0);
$this->RegisterAttributeInteger('EinschaltenGesperrtBis', 0);
$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( $this->RegisterTimer(
'Statuszyklus', 'Statuszyklus',
@@ -108,6 +126,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);" "IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
); );
$this->RegisterTimer(
'RegeluebergangTimeout',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
);
} }
public function ApplyChanges(): void public function ApplyChanges(): void
@@ -116,10 +139,30 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->initialisiereLokaleEinstellungen(); $this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
// Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen. // Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen.
$this->setzeZustand('FahrzeugGeladen', false); $this->setzeZustand('FahrzeugGeladen', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeInteger('LetzteGesetzteLeistung', 0);
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
if ($this->ReadAttributeInteger('PhasenerkennungVersion') < 1) {
$this->WriteAttributeInteger(
'ZustandPhasenzahl',
LadestationRegler::PHASEN_UNBEKANNT
);
$this->WriteAttributeInteger('PhasenerkennungVersion', 1);
}
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->WriteAttributeInteger(
'ErkannterMaximalstrom',
$this->ReadPropertyInteger('MaximalerLadestrom')
);
$this->setzeFahrzeugstromKandidatZurueck();
$this->beendeRegeluebergang();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -142,18 +185,37 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
{ {
switch ($ident) { switch ($ident) {
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $aktiv = (bool) $wert;
if (!(bool) $wert) { $this->SetValue('Aktiv', $aktiv);
$this->setzeZustand('SollwertGueltig', false); if ($aktiv) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->verwerfeSollwert();
} }
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
case 'Ladefreigabe': case 'Ladefreigabe':
$ladefreigabe = (bool) $wert;
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
$this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
if ($ladefreigabe) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->verwerfeSollwert();
}
$this->aktualisiere(true);
return;
case 'Solarladen': case 'Solarladen':
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert); $solarladen = (bool) $wert;
$this->setzeSichtbareVariable($ident, (bool) $wert); if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
$this->setzeZustand('SollwertGueltig', false); $this->starteRegeluebergang();
}
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
$this->setzeSichtbareVariable('Solarladen', $solarladen);
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -163,7 +225,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
case 'VorgabeTimeout': case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(true);
return;
case 'RegeluebergangTimeout':
$this->beendeRegeluebergang();
$this->aktualisiere(true); $this->aktualisiere(true);
return; return;
@@ -171,7 +238,6 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if ((bool) $wert) { if ((bool) $wert) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 0); $this->SetTimerInterval('RueckmeldungVerzoegert', 0);
} }
$this->aktualisiere(false);
$this->sendeVerbraucherdaten(); $this->sendeVerbraucherdaten();
return; return;
@@ -204,13 +270,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
} }
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']); $this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
$this->aktualisiere(false, false); $this->aktualisiere(false, false, false);
$sollleistung = $daten['Sollleistung_W']; $sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) { if ($sollleistung === null) {
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->aktualisiere(false, false); $this->aktualisiere(false, false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -222,6 +288,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
); );
} }
$this->beendeRegeluebergang();
$this->WriteAttributeInteger('LetzteManagerID', $managerID); $this->WriteAttributeInteger('LetzteManagerID', $managerID);
$this->WriteAttributeInteger('LetzteVorgabeZeit', time()); $this->WriteAttributeInteger('LetzteVorgabeZeit', time());
$this->setzeZustand('Sollleistung', $sollleistung); $this->setzeZustand('Sollleistung', $sollleistung);
@@ -231,7 +298,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $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 { try {
if ($statusLesen) { if ($statusLesen) {
@@ -243,7 +314,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'); $verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
$geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen'); $geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen');
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl'); $phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom'); $maximalstrom = min(
$this->ReadPropertyInteger('MaximalerLadestrom'),
max(6, $this->ReadAttributeInteger('GeraeteMaximalstrom')),
max(6, $this->ReadAttributeInteger('ErkannterMaximalstrom'))
);
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $maximalstrom);
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen'); $solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
$peakbetrieb = $this->ReadAttributeString('Betriebsart') $peakbetrieb = $this->ReadAttributeString('Betriebsart')
=== Nachrichtenvertrag::BETRIEBSART_PEAK; === Nachrichtenvertrag::BETRIEBSART_PEAK;
@@ -267,6 +343,40 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$solarladen, $solarladen,
$peakbetrieb $peakbetrieb
); );
if ($aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& !($peakbetrieb && $solarladen)
&& $phasenzahl !== LadestationRegler::PHASEN_UNBEKANNT
) {
$jetzt = time();
$letzteLeistung = $this->ReadAttributeInteger('LetzteGesetzteLeistung');
if ($jetzt < $this->ReadAttributeInteger('LeistungswechselGesperrtBis')) {
$angebot = [$letzteLeistung];
} elseif ($letzteLeistung > 0
&& $jetzt < $this->ReadAttributeInteger('AusschaltenGesperrtBis')
) {
$positiveStufen = array_values(array_filter(
LadestationRegler::leistungsangebot(
$aktiv,
$ladefreigabe,
$verbunden,
$geladen,
$phasenzahl,
$maximalstrom,
$solarladen,
false
),
static fn (int $wert): bool => $wert > 0
));
$angebot = $positiveStufen !== [] ? $positiveStufen : [$letzteLeistung];
} elseif ($letzteLeistung <= 0
&& $jetzt < $this->ReadAttributeInteger('EinschaltenGesperrtBis')
) {
$angebot = [0];
}
}
$angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR); $angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR);
$this->WriteAttributeString('Leistungsangebot', $angebotJson); $this->WriteAttributeString('Leistungsangebot', $angebotJson);
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson); $this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
@@ -274,7 +384,19 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1); $this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
$sollleistung = $this->bestimmeWirksameSollleistung($angebot); $sollleistung = $this->bestimmeWirksameSollleistung($angebot);
$phasenerkennungAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT;
$regeluebergangAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
if ($steuerungAusfuehren && !$phasenerkennungAktiv && !$regeluebergangAktiv) {
$this->setzeLadestrom($sollleistung); $this->setzeLadestrom($sollleistung);
}
$this->setzeStoerung(''); $this->setzeStoerung('');
$this->SetStatus(self::STATUS_AKTIV); $this->SetStatus(self::STATUS_AKTIV);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
@@ -303,18 +425,134 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->ReadPropertyInteger('Geraetetyp'), $this->ReadPropertyInteger('Geraetetyp'),
$antwort $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;
$bekanntePhasenzahl = $warVerbunden
? $this->ReadAttributeInteger('ZustandPhasenzahl')
: LadestationRegler::PHASEN_UNBEKANNT;
if (!$verbunden) {
$bekanntePhasenzahl = LadestationRegler::PHASEN_UNBEKANNT;
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
} elseif ($bekanntePhasenzahl === LadestationRegler::PHASEN_UNBEKANNT) {
$direktErkanntePhasen = (int) ($rohstatus['Phasenzahl']
?? LadestationRegler::PHASEN_UNBEKANNT);
if (in_array(
$direktErkanntePhasen,
[LadestationRegler::PHASEN_EIN, LadestationRegler::PHASEN_DREI],
true
)) {
$bekanntePhasenzahl = $direktErkanntePhasen;
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
} elseif ($rohstatus['Istleistung_W'] > 7500.0) {
$bekanntePhasenzahl = LadestationRegler::PHASEN_DREI;
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
} elseif ((bool) $this->GetValue('Aktiv')
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
) {
$erkennungSeit = $this->ReadAttributeInteger('PhasenerkennungSeit');
if ($erkennungSeit <= 0) {
$this->sendeLadestromAnGeraet(self::PHASENERKENNUNGSSTROM);
$erkennungSeit = time();
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
} elseif ((time() - $erkennungSeit)
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
) {
$bekanntePhasenzahl = LadestationRegler::erkennePhasenzahlAusLeistung(
(float) $rohstatus['Istleistung_W'],
$this->ReadAttributeInteger('LetzterGesetzterStrom')
);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
} else {
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
}
$status = LadestationRegler::normalisiereStatus( $status = LadestationRegler::normalisiereStatus(
$rohstatus, $rohstatus,
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'), $warVerbunden,
$this->ReadAttributeBoolean('ZustandFahrzeugGeladen'), $this->ReadAttributeBoolean('ZustandFahrzeugGeladen'),
$this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6 $this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6,
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt'),
$bekanntePhasenzahl
); );
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
} elseif ($status['Ladestrom_A'] >= 3.5) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true);
}
if (!$status['FahrzeugVerbunden']
|| $status['Phasenzahl'] !== LadestationRegler::PHASEN_UNBEKANNT
) {
$this->aktualisiereFahrzeugstromgrenze($status);
}
foreach ($status as $ident => $wert) { foreach ($status as $ident => $wert) {
$this->setzeZustand($ident, $wert); $this->setzeZustand($ident, $wert);
} }
} }
/** @param array<string, int|float|bool> $status */
private function aktualisiereFahrzeugstromgrenze(array $status): void
{
$konfigurierterMaximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeInteger('ErkannterMaximalstrom', $konfigurierterMaximalstrom);
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $konfigurierterMaximalstrom);
$this->setzeFahrzeugstromKandidatZurueck();
return;
}
$kandidat = LadestationRegler::ermittleFahrzeugMaximalstrom(
$this->ReadAttributeInteger('LetzterGesetzterStrom'),
(float) $status['Ladestrom_A'],
$konfigurierterMaximalstrom
);
if ($kandidat === null
|| $kandidat >= $this->ReadAttributeInteger('ErkannterMaximalstrom')
) {
$this->setzeFahrzeugstromKandidatZurueck();
return;
}
$jetzt = time();
if ($this->ReadAttributeInteger('FahrzeugstromKandidat') !== $kandidat) {
$this->WriteAttributeInteger('FahrzeugstromKandidat', $kandidat);
$this->WriteAttributeInteger('FahrzeugstromKandidatSeit', $jetzt);
return;
}
if (($jetzt - $this->ReadAttributeInteger('FahrzeugstromKandidatSeit'))
< $this->ReadPropertyInteger('FahrzeugstromErkennungszeit')
) {
return;
}
$this->WriteAttributeInteger('ErkannterMaximalstrom', $kandidat);
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $kandidat);
$this->setzeFahrzeugstromKandidatZurueck();
}
private function setzeFahrzeugstromKandidatZurueck(): void
{
$this->WriteAttributeInteger('FahrzeugstromKandidat', 0);
$this->WriteAttributeInteger('FahrzeugstromKandidatSeit', 0);
}
/** @param list<int> $angebot */ /** @param list<int> $angebot */
private function bestimmeWirksameSollleistung(array $angebot): int private function bestimmeWirksameSollleistung(array $angebot): int
{ {
@@ -323,7 +561,15 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
if (in_array($sollleistung, $angebot, true)) { if (in_array($sollleistung, $angebot, true)) {
return $sollleistung; return $sollleistung;
} }
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ersatz = 0;
foreach ($angebot as $leistungswert) {
if ($leistungswert > $sollleistung) {
break;
}
$ersatz = $leistungswert;
}
return $ersatz;
} }
if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) { if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) {
@@ -339,12 +585,61 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$strom = $leistung === 0 $strom = $leistung === 0
? 0 ? 0
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl); : LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) { $bisherigeLeistung = $this->ReadAttributeInteger('LetzteGesetzteLeistung');
$stromUnveraendert = $strom === $this->ReadAttributeInteger('LetzterGesetzterStrom');
if ($stromUnveraendert && $leistung === $bisherigeLeistung) {
return;
}
if (!$stromUnveraendert) {
$this->sendeLadestromAnGeraet($strom);
}
if ($leistung === $bisherigeLeistung) {
return; return;
} }
if (!$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden')) {
$leistungswechselGesperrtBis = 0;
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
} else {
$jetzt = time();
$wechselzeit = $this->ReadPropertyInteger('ZeitZwischenZustandswechseln');
$leistungswechselGesperrtBis = $wechselzeit > 0
? $jetzt + ($wechselzeit * 60)
: 0;
if ($bisherigeLeistung <= 0 && $leistung > 0) {
$mindestzeit = $this->ReadPropertyInteger('Mindesteinschaltdauer');
$this->WriteAttributeInteger(
'AusschaltenGesperrtBis',
$mindestzeit > 0 ? $jetzt + ($mindestzeit * 60) : 0
);
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
} elseif ($bisherigeLeistung > 0 && $leistung <= 0) {
$mindestzeit = $this->ReadPropertyInteger('Mindestausschaltdauer');
$this->WriteAttributeInteger(
'EinschaltenGesperrtBis',
$mindestzeit > 0 ? $jetzt + ($mindestzeit * 60) : 0
);
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
}
}
$this->WriteAttributeInteger('LetzteGesetzteLeistung', $leistung);
$this->WriteAttributeInteger(
'LeistungswechselGesperrtBis',
$leistungswechselGesperrtBis
);
}
private function sendeLadestromAnGeraet(int $strom): void
{
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->setzeFahrzeugstromKandidatZurueck();
$transport = new LadestationTransport( $transport = new LadestationTransport(
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage) fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage, true)
); );
$transport->ladestromSetzen( $transport->ladestromSetzen(
$this->ReadPropertyInteger('Geraetetyp'), $this->ReadPropertyInteger('Geraetetyp'),
@@ -357,8 +652,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
/** /**
* @param array<string, mixed> $anfrage * @param array<string, mixed> $anfrage
*/ */
private function fuehreHttpAnfrageAus(array $anfrage): string private function fuehreHttpAnfrageAus(
array $anfrage,
bool $alsGeraetebefehl = false
): string
{ {
if ($alsGeraetebefehl) {
$protokoll = [ $protokoll = [
'Methode' => $anfrage['Methode'], 'Methode' => $anfrage['Methode'],
'URL' => $anfrage['URL'], 'URL' => $anfrage['URL'],
@@ -367,6 +666,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
json_encode($protokoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) json_encode($protokoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
); );
}
if ($this->ReadPropertyBoolean('Testmodus')) { if ($this->ReadPropertyBoolean('Testmodus')) {
$url = (string) $anfrage['URL']; $url = (string) $anfrage['URL'];
@@ -570,6 +870,23 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
return is_array($zuordnung) ? $zuordnung : []; 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 private function pruefeKonfiguration(): void
{ {
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) { foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
@@ -577,11 +894,27 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.'); throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
} }
} }
foreach (['Meldeintervall', 'VorgabeTimeout', 'Abfrageintervall'] as $property) { foreach ([
'Meldeintervall',
'VorgabeTimeout',
'Abfrageintervall',
'Phasenerkennungszeit',
'FahrzeugstromErkennungszeit',
] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) { if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
} }
} }
foreach ([
'ZeitZwischenZustandswechseln',
'Mindesteinschaltdauer',
'Mindestausschaltdauer',
] as $property) {
if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
}
}
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom'); $maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
if ($maximalstrom < 6 || $maximalstrom > 32) { if ($maximalstrom < 6 || $maximalstrom > 32) {
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.'); throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
@@ -617,28 +950,31 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private function aktualisiereVariablen(): void 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')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose(); $this->registriereVerbraucherDiagnose();
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200); $this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210); $this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 210);
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 220);
$this->RegisterVariableInteger(
'FahrzeugMaximalstrom',
'Erkannter Fahrzeug-Maximalstrom',
'',
230
);
$this->SetValue(
'FahrzeugMaximalstrom',
$this->ReadAttributeInteger('ErkannterMaximalstrom')
);
$this->SetValue( $this->SetValue(
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
$this->ReadAttributeString('Leistungsangebot') $this->ReadAttributeString('Leistungsangebot')
); );
foreach (self::DIAGNOSE_VARIABLEN as $ident) { foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if (!in_array($ident, ['LetzterGeraetebefehl', 'LeistungsangebotDiagnose'], true)) { if (!in_array($ident, [
'LetzterGeraetebefehl',
'LeistungsangebotDiagnose',
'FahrzeugMaximalstrom',
], true)) {
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident)); $this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
} }
} }
@@ -647,6 +983,24 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->entferneVariable($ident); $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 private function entferneVariable(string $ident): void
@@ -754,7 +1108,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private function deaktiviereTimer(): void private function deaktiviereTimer(): void
{ {
foreach (['Statuszyklus', 'Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) { foreach ([
'Statuszyklus',
'Meldezyklus',
'RueckmeldungVerzoegert',
'VorgabeTimeout',
'RegeluebergangTimeout',
] as $timer) {
$this->SetTimerInterval($timer, 0); $this->SetTimerInterval($timer, 0);
} }
} }
+105 -9
View File
@@ -260,10 +260,14 @@
"type": "Label", "type": "Label",
"caption": "Energiezaehler werden in kWh erwartet; ueber den Faktor koennen beispielsweise Wh (0,001) umgerechnet werden." "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", "type": "List",
"name": "AnlagenWechselrichter", "name": "AnlagenWechselrichter",
"caption": "Wechselrichter", "caption": "Wechselrichter / AC-Wandler",
"rowCount": 4, "rowCount": 4,
"add": true, "add": true,
"delete": 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", "type": "List",
"name": "AnlagenBatterien", "name": "AnlagenBatterien",
@@ -722,26 +730,70 @@
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergyPieAnzeigen", "name": "EnergyPieAnzeigen",
"caption": "Energy Pie unter dem Manager anzeigen", "caption": "Energy Pie unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergyPieAnzeigen', $EnergyPieAnzeigen);"
}, },
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergiediagrammeAnzeigen", "name": "EnergiediagrammeAnzeigen",
"caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen", "caption": "Leistungs- und Energiediagramm unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergiediagrammeAnzeigen', $EnergiediagrammeAnzeigen);" },
{
"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", "type": "CheckBox",
"name": "FunFactsAnzeigen", "name": "FunFactsAnzeigen",
"caption": "Energy Facts unter dem Manager anzeigen", "caption": "Energy Facts unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormFunFactsAnzeigen', $FunFactsAnzeigen);"
}, },
{ {
"type": "CheckBox", "type": "CheckBox",
"name": "EnergieflussAnzeigen", "name": "EnergieflussAnzeigen",
"caption": "Energiefluss unter dem Manager anzeigen", "caption": "Energiefluss unter dem Manager anzeigen"
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);" },
{
"type": "Select",
"name": "EnergieflussLeistungseinheit",
"caption": "Leistungseinheit im Energiefluss",
"options": [
{"caption": "Watt (W)", "value": 0},
{"caption": "Kilowatt (kW)", "value": 1}
]
} }
] ]
}, },
@@ -783,6 +835,50 @@
"type": "ExpansionPanel", "type": "ExpansionPanel",
"caption": "SDL / VGT", "caption": "SDL / VGT",
"items": [ "items": [
{
"type": "CheckBox",
"name": "SDLAktiv",
"caption": "SDL aktiv",
"onChange": "IPS_RequestAction($id, 'FormSDLAktiv', $SDLAktiv);"
},
{
"type": "ExpansionPanel",
"name": "SDLKonfiguration",
"caption": "SDL / Regelenergie Messung",
"visible": false,
"items": [
{
"type": "SelectVariable",
"name": "SDLLeistungVariableID",
"caption": "SDL Istleistung"
},
{
"type": "NumberSpinner",
"name": "SDLLeistungsfaktor",
"caption": "SDL Leistungsfaktor nach Watt",
"digits": 3
},
{
"type": "SelectVariable",
"name": "SDLSOCVariableID",
"caption": "SDL SOC in Prozent (optional)"
},
{
"type": "CheckBox",
"name": "SDLEnergieflussAnzeigen",
"caption": "SDL mit SOC im Energiefluss anzeigen"
},
{
"type": "CheckBox",
"name": "SDLDiagrammeAnzeigen",
"caption": "SDL mit SOC in den Diagrammen anzeigen"
},
{
"type": "Label",
"caption": "Nach Skalierung: positiv = Laden / Bezug, negativ = Entladen / Abgabe. SDL muss separat gemessen sein und darf nicht bereits in der normalen Batterieleistung enthalten sein. Die Hauslast fuer Prognosen wird unabhaengig von den Anzeigeoptionen korrigiert."
}
]
},
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"name": "SDLAnschluss", "name": "SDLAnschluss",
+264 -63
View File
@@ -11,6 +11,7 @@ require_once __DIR__ . '/../libs/Anlagentopologie.php';
require_once __DIR__ . '/../libs/StoerungsSnapshot.php'; require_once __DIR__ . '/../libs/StoerungsSnapshot.php';
require_once __DIR__ . '/../libs/EnergieMessung.php'; require_once __DIR__ . '/../libs/EnergieMessung.php';
require_once __DIR__ . '/../libs/ManagerEnergieTrait.php'; require_once __DIR__ . '/../libs/ManagerEnergieTrait.php';
require_once __DIR__ . '/../libs/ManagerSDLTrait.php';
use Belevo\EnelixEMS\Anlagentopologie; use Belevo\EnelixEMS\Anlagentopologie;
use Belevo\EnelixEMS\EinspeiseRegler; use Belevo\EnelixEMS\EinspeiseRegler;
@@ -25,6 +26,7 @@ use Belevo\EnelixEMS\StoerungsSnapshot;
class Manager extends IPSModule implements ManagerSchnittstelle class Manager extends IPSModule implements ManagerSchnittstelle
{ {
use ManagerEnergieTrait; use ManagerEnergieTrait;
use \Belevo\EnelixEMS\ManagerSDLTrait;
private const STATUS_AKTIV = 102; private const STATUS_AKTIV = 102;
private const STATUS_MESSWERT_UNGUELTIG = 201; private const STATUS_MESSWERT_UNGUELTIG = 201;
@@ -46,6 +48,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv', 'HausverbrauchLeistungArchiv',
'NetzleistungArchiv', 'NetzleistungArchiv',
'BatterieleistungArchiv', 'BatterieleistungArchiv',
'SDLLeistungArchiv',
'SDLSOCArchiv',
]; ];
private const ENERGIE_VARIABLEN = [ private const ENERGIE_VARIABLEN = [
'PVEnergie', 'PVEnergie',
@@ -54,6 +58,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie', 'EinspeisungEnergie',
'BatterieLadenEnergie', 'BatterieLadenEnergie',
'BatterieEntladenEnergie', 'BatterieEntladenEnergie',
'SDLLadenEnergie',
'SDLEntladenEnergie',
]; ];
private const VERBRAUCHER_LIZENZEN = [ private const VERBRAUCHER_LIZENZEN = [
'{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [ '{B7C54AF4-AD7D-4FE4-B75D-203693906251}' => [
@@ -146,6 +152,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('PrognoseSendeintervall', 60); $this->RegisterPropertyInteger('PrognoseSendeintervall', 60);
$this->RegisterPropertyString('PrognoseAnschluss', '{}'); $this->RegisterPropertyString('PrognoseAnschluss', '{}');
$this->RegisterPropertyString('SDLAnschluss', '{}'); $this->RegisterPropertyString('SDLAnschluss', '{}');
$this->RegisterPropertyBoolean('SDLAktiv', false);
$this->RegisterPropertyInteger('SDLLeistungVariableID', 0);
$this->RegisterPropertyFloat('SDLLeistungsfaktor', 1.0);
$this->RegisterPropertyInteger('SDLSOCVariableID', 0);
$this->RegisterPropertyBoolean('SDLEnergieflussAnzeigen', false);
$this->RegisterPropertyBoolean('SDLDiagrammeAnzeigen', false);
$this->RegisterPropertyString('Lizenzcode', ''); $this->RegisterPropertyString('Lizenzcode', '');
// Verdeckte Altproperty fuer bestehende Instanzkonfigurationen. // Verdeckte Altproperty fuer bestehende Instanzkonfigurationen.
$this->RegisterPropertyString('LizenzAnschluss', '{}'); $this->RegisterPropertyString('LizenzAnschluss', '{}');
@@ -171,8 +183,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterPropertyInteger('EnergieLoeschenMonate', 0); $this->RegisterPropertyInteger('EnergieLoeschenMonate', 0);
$this->RegisterPropertyBoolean('EnergyPieAnzeigen', false); $this->RegisterPropertyBoolean('EnergyPieAnzeigen', false);
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', 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('FunFactsAnzeigen', false);
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false); $this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
$this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0);
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10); $this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
$this->EnableAction('Aktiv'); $this->EnableAction('Aktiv');
@@ -180,6 +197,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30); $this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
$this->RegisterAttributeString('VerbraucherCache', '{}'); $this->RegisterAttributeString('VerbraucherCache', '{}');
$this->RegisterAttributeString('VerbraucherEnergie', '{}');
$this->RegisterAttributeString('LetzteSollwerte', '{}'); $this->RegisterAttributeString('LetzteSollwerte', '{}');
$this->RegisterAttributeString('LetztePvGrenzen', '{}'); $this->RegisterAttributeString('LetztePvGrenzen', '{}');
$this->RegisterAttributeString('Netzfahrplan', '{}'); $this->RegisterAttributeString('Netzfahrplan', '{}');
@@ -201,6 +219,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0); $this->RegisterAttributeInteger('MessungLetzteEnergieaufzeichnung', 0);
$this->RegisterAttributeString('MessungEnergiePuffer', '{}'); $this->RegisterAttributeString('MessungEnergiePuffer', '{}');
$this->RegisterAttributeString('MessungZaehlerstaende', '{}'); $this->RegisterAttributeString('MessungZaehlerstaende', '{}');
$this->RegisterAttributeString('EnergiediagrammKonfiguration', '{}');
$this->RegisterAttributeString('EnergieflussKonfiguration', '{}');
$this->RegisterAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->RegisterAttributeString('SDLMesskonfiguration', '');
// Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren. // Die alten Timer bleiben deaktiviert registriert, damit bestehende Instanzen sauber migrieren.
$this->RegisterTimer('Regelzyklus', 0, ''); $this->RegisterTimer('Regelzyklus', 0, '');
@@ -210,6 +232,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
0, 0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);" "IPS_RequestAction(\$_IPS['TARGET'], 'Regeln', true);"
); );
$this->RegisterTimer(
'RegelungVerzoegert',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegelnVerzoegert', false);"
);
$this->RegisterTimer( $this->RegisterTimer(
'PrognoseSenden', 'PrognoseSenden',
0, 0,
@@ -236,13 +263,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
$this->konfiguriereEnergiearchiv(); $this->synchronisiereSDLMesskonfiguration();
$this->synchronisiereEnergievisualisierungen(); $this->synchronisiereEnergievisualisierungen();
$this->konfiguriereEnergiearchiv();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG); $this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', 0); $this->SetTimerInterval('KeepAlive', 0);
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->SetTimerInterval('PrognoseSenden', 0); $this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval('StoerungenSenden', 0); $this->SetTimerInterval('StoerungenSenden', 0);
$this->SetTimerInterval('MesswerteAufzeichnen', 0); $this->SetTimerInterval('MesswerteAufzeichnen', 0);
@@ -254,6 +283,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->SetTimerInterval('Regelzyklus', 0); $this->SetTimerInterval('Regelzyklus', 0);
$this->SetTimerInterval('VorgabenErneuern', 0); $this->SetTimerInterval('VorgabenErneuern', 0);
$this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000); $this->SetTimerInterval('KeepAlive', $this->ReadPropertyInteger('KeepAlive') * 1000);
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->SetTimerInterval('PrognoseSenden', 0); $this->SetTimerInterval('PrognoseSenden', 0);
$this->SetTimerInterval( $this->SetTimerInterval(
'StoerungenSenden', 'StoerungenSenden',
@@ -266,8 +296,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen')); $this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
$this->aktualisiereAnschlussstatus(); $this->aktualisiereAnschlussstatus();
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv') $geraetezugangFehlt = $this->geraetezugangBenoetigt()
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
&& $this->ReadAttributeString('PrognoseInstallationsToken') === ''; && $this->ReadAttributeString('PrognoseInstallationsToken') === '';
if (!$this->aktualisiereLizenzfreigabe( if (!$this->aktualisiereLizenzfreigabe(
$this->ReadPropertyString('Lizenzcode'), $this->ReadPropertyString('Lizenzcode'),
@@ -415,6 +444,13 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->monatsgrenzenFuerFormular() $this->monatsgrenzenFuerFormular()
); );
$this->setzeFormularParameter(
$formular['elements'],
'SDLKonfiguration',
'visible',
$this->ReadPropertyBoolean('SDLAktiv')
);
return json_encode($formular, JSON_THROW_ON_ERROR); return json_encode($formular, JSON_THROW_ON_ERROR);
} }
@@ -431,6 +467,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID') if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
&& (int) $nachricht === self::VM_UPDATE && (int) $nachricht === self::VM_UPDATE
) { ) {
$this->aktualisiereEnergieflusswerte();
$this->regeln(false); $this->regeln(false);
} }
} }
@@ -447,6 +484,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->regeln((bool) $wert); $this->regeln((bool) $wert);
return; return;
case 'RegelnVerzoegert':
$this->SetTimerInterval('RegelungVerzoegert', 0);
$this->regeln(false);
return;
case 'PrognoseSenden': case 'PrognoseSenden':
$this->sendePrognoseTelemetrie(); $this->sendePrognoseTelemetrie();
return; return;
@@ -480,6 +522,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
return; return;
case 'FormSDLAktiv':
$this->UpdateFormField('SDLKonfiguration', 'visible', (bool) $wert);
return;
case 'FormEnergyPieAnzeigen': case 'FormEnergyPieAnzeigen':
$this->synchronisiereEnergyPie((bool) $wert); $this->synchronisiereEnergyPie((bool) $wert);
return; return;
@@ -500,7 +546,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
if (!is_string($wert)) { if (!is_string($wert)) {
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.'); throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
} }
$this->aktualisiereLizenzfreigabe($wert, true); if ($this->aktualisiereLizenzfreigabe($wert, true)) {
$this->synchronisiereAnlagentopologie();
}
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert)); $this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
$this->aktualisiereLastspitzenformular($wert); $this->aktualisiereLastspitzenformular($wert);
$this->aktualisierePrognoseformular($wert); $this->aktualisierePrognoseformular($wert);
@@ -602,13 +650,80 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.'); throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
} }
$jetzt = time();
$this->aktualisiereVerbraucherEnergie($absenderID, $daten, $jetzt);
$cache = $this->leseJsonAttribut('VerbraucherCache'); $cache = $this->leseJsonAttribut('VerbraucherCache');
$cache[(string) $absenderID] = [ $cache[(string) $absenderID] = [
'EmpfangenAm' => time(), 'EmpfangenAm' => $jetzt,
'Daten' => $daten, 'Daten' => $daten,
]; ];
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR)); $this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
$this->regeln(false); $this->aktualisiereEnergieflusswerte();
$this->aktualisiereDiagrammMesswerte();
// Den synchronen Rueckruf in die noch meldende Verbraucherinstanz vermeiden.
$this->SetTimerInterval('RegelungVerzoegert', 100);
}
/** @param array<string, mixed> $daten */
private function aktualisiereVerbraucherEnergie(int $instanzID, array $daten, int $jetzt): void
{
$energien = $this->leseJsonAttribut('VerbraucherEnergie');
$bisher = $energien[(string) $instanzID] ?? [];
$energieWh = is_array($bisher) && is_numeric($bisher['Energie_Wh'] ?? null)
? (float) $bisher['Energie_Wh']
: 0.0;
$letzterZeitpunkt = is_array($bisher) && is_int($bisher['Zeitpunkt'] ?? null)
? $bisher['Zeitpunkt']
: 0;
$letzteLeistung = is_array($bisher) && is_numeric($bisher['Leistung_W'] ?? null)
? (float) $bisher['Leistung_W']
: 0.0;
if ($letzterZeitpunkt > 0 && $jetzt > $letzterZeitpunkt) {
$dauer = min(
$jetzt - $letzterZeitpunkt,
$this->ReadPropertyInteger('VerbraucherTimeout')
);
$energieWh += $letzteLeistung * $dauer / 3600.0;
}
$energien[(string) $instanzID] = [
'Energie_Wh' => $energieWh,
'Zeitpunkt' => $jetzt,
'Leistung_W' => $this->verbraucherleistung($daten),
];
$this->WriteAttributeString(
'VerbraucherEnergie',
json_encode($energien, JSON_THROW_ON_ERROR)
);
}
/** @param array<string, mixed> $daten */
private function verbraucherleistung(array $daten): float
{
if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN
&& is_numeric($daten['Istleistung_W'])
) {
return (float) $daten['Istleistung_W'];
}
return (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0);
}
/** @return array<int, float> */
private function bezogeneVerbraucherEnergienWh(): array
{
$ergebnis = [];
foreach ($this->leseJsonAttribut('VerbraucherEnergie') as $instanzID => $eintrag) {
if (!is_array($eintrag) || !is_numeric($eintrag['Energie_Wh'] ?? null)) {
continue;
}
$ergebnis[(int) $instanzID] = (float) $eintrag['Energie_Wh'];
}
return $ergebnis;
} }
private function regeln(bool $erneuern): void private function regeln(bool $erneuern): void
@@ -698,7 +813,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$netzleistung, $netzleistung,
$zielnetzleistung, $zielnetzleistung,
$verbraucher, $verbraucher,
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV' $betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV',
$this->bezogeneVerbraucherEnergienWh()
); );
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']); $this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([ $this->setzeDiagnosewert('Verbraucherstatus', json_encode([
@@ -939,13 +1055,20 @@ class Manager extends IPSModule implements ManagerSchnittstelle
foreach ($ids as $instanzID) { foreach ($ids as $instanzID) {
$eintrag = $cache[(string) $instanzID] ?? null; $eintrag = $cache[(string) $instanzID] ?? null;
$grund = ''; $grund = $this->verbraucherMeldegrund($eintrag, $betriebsart, $timeout);
if (!is_array($eintrag) || !isset($eintrag['EmpfangenAm'], $eintrag['Daten'])) { $synchronisierungsfehler = '';
$grund = 'Noch keine Meldung'; if (in_array($grund, ['Noch keine Meldung', 'Meldung ist veraltet'], true)) {
} elseif (time() - (int) $eintrag['EmpfangenAm'] > $timeout) { try {
$grund = 'Meldung ist veraltet'; $this->SetBuffer('VerbraucherSynchronisierungAktiv', '1');
} elseif (($eintrag['Daten']['Betriebsart'] ?? null) !== $betriebsart) { IPS_RequestAction($instanzID, 'Melden', true);
$grund = 'Leistungsangebot gehoert nicht zur Betriebsart ' . $betriebsart; } 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 !== '') { if ($grund !== '') {
@@ -956,8 +1079,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
try { try {
$this->sendeManagerdaten($instanzID, $betriebsart, null); $this->sendeManagerdaten($instanzID, $betriebsart, null);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$stoerungen[] = $detail['Name'] . ': Betriebsart konnte nicht uebermittelt werden: ' $synchronisierungsfehler = $fehler->getMessage();
. $fehler->getMessage(); }
if ($synchronisierungsfehler !== '') {
$stoerungen[] = $detail['Name'] . ': Vollmeldung konnte nicht angefordert werden: '
. $synchronisierungsfehler;
} }
continue; continue;
} }
@@ -970,6 +1096,27 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return $aktuell; 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 private function setzeNichtGeregelteVerbraucher(string $grund): void
{ {
try { try {
@@ -1554,6 +1701,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Hausverbrauch', 'HausverbrauchLeistungArchiv' => 'Hausverbrauch',
'NetzleistungArchiv' => 'Netzleistung Aufzeichnung', 'NetzleistungArchiv' => 'Netzleistung Aufzeichnung',
'BatterieleistungArchiv' => 'Batterieleistung', 'BatterieleistungArchiv' => 'Batterieleistung',
'SDLLeistungArchiv' => 'SDL Istleistung',
]; ];
$energievariablen = [ $energievariablen = [
'PVEnergie' => 'PV-Energie', 'PVEnergie' => 'PV-Energie',
@@ -1562,6 +1710,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'EinspeisungEnergie' => 'Einspeisung Energie', 'EinspeisungEnergie' => 'Einspeisung Energie',
'BatterieLadenEnergie' => 'Batterie geladen', 'BatterieLadenEnergie' => 'Batterie geladen',
'BatterieEntladenEnergie' => 'Batterie entladen', 'BatterieEntladenEnergie' => 'Batterie entladen',
'SDLLadenEnergie' => 'SDL geladen',
'SDLEntladenEnergie' => 'SDL entladen',
]; ];
$position = 200; $position = 200;
foreach ($leistungsvariablen as $ident => $name) { foreach ($leistungsvariablen as $ident => $name) {
@@ -1575,6 +1725,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$position += 10; $position += 10;
} }
$this->RegisterVariableFloat('SDLSOCArchiv', 'SDL Ladezustand', '~Intensity.100', 390);
IPS_SetHidden($this->GetIDForIdent('SDLSOCArchiv'), !$this->ReadPropertyBoolean('MesswerteAnzeigen'));
if ($this->ReadPropertyBoolean('SDLAktiv')) {
$this->RegisterVariableString('SDLStatus', 'SDL-Status', '', 120);
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) { if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->RegisterVariableBoolean( $this->RegisterVariableBoolean(
'NetzleistungGueltig', 'NetzleistungGueltig',
@@ -1633,6 +1789,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
foreach (self::DIAGNOSE_VARIABLEN as $ident) { foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if ($ident === 'SDLStatus' && $this->ReadPropertyBoolean('SDLAktiv')) {
continue;
}
$variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); $variablenID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) { if (is_int($variablenID) && $variablenID > 0 && IPS_VariableExists($variablenID)) {
$this->UnregisterVariable($ident); $this->UnregisterVariable($ident);
@@ -1908,7 +2067,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
]; ];
} }
/** @return array{PV: float, Haus: float, Netz: float, Batterie: float} */ /** @return array{PV: float, Haus: float, Netz: float, Batterie: float, SDL: float} */
private function leseMessleistungen(): array private function leseMessleistungen(): array
{ {
$pv = 0.0; $pv = 0.0;
@@ -1921,12 +2080,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
$netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID')) $netz = (float) GetValue($this->ReadPropertyInteger('NetzleistungVariableID'))
* $this->ReadPropertyFloat('Netzleistungsfaktor'); * $this->ReadPropertyFloat('Netzleistungsfaktor');
$sdl = $this->aktuelleSDLLeistung();
return [ return [
'PV' => $pv, 'PV' => $pv,
'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie), 'Haus' => EnergieMessung::hausleistung($pv, $netz, $batterie, $sdl),
'Netz' => $netz, 'Netz' => $netz,
'Batterie' => $batterie, 'Batterie' => $batterie,
'SDL' => $sdl,
]; ];
} }
@@ -1951,7 +2112,11 @@ class Manager extends IPSModule implements ManagerSchnittstelle
} }
/** @return array<string, float>|null */ /** @return array<string, float>|null */
private function leseEnergieZaehlerDeltas(): ?array private function leseEnergieZaehlerDeltas(
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0,
bool $hausVollstaendig = true
): ?array
{ {
$quellen = $this->energieZaehlerquellen(); $quellen = $this->energieZaehlerquellen();
if ($quellen === null) { if ($quellen === null) {
@@ -1980,6 +2145,8 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$delta = $stand - (float) $alt[$schluessel]; $delta = $stand - (float) $alt[$schluessel];
if ($delta >= 0.0) { if ($delta >= 0.0) {
$deltas[$ident] += $delta; $deltas[$ident] += $delta;
} elseif ($this->ReadPropertyBoolean('SDLAktiv')) {
$hausVollstaendig = false;
} }
} }
} }
@@ -1991,13 +2158,17 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0); return array_fill_keys(self::ENERGIE_VARIABLEN, 0.0);
} }
$deltas['Hausenergie'] = EnergieMessung::hausenergie( $deltas['SDLLadenEnergie'] = $sdlLaden;
$deltas['SDLEntladenEnergie'] = $sdlEntladen;
$deltas['Hausenergie'] = $hausVollstaendig ? EnergieMessung::hausenergie(
$deltas['PVEnergie'], $deltas['PVEnergie'],
$deltas['NetzbezugEnergie'], $deltas['NetzbezugEnergie'],
$deltas['EinspeisungEnergie'], $deltas['EinspeisungEnergie'],
$deltas['BatterieLadenEnergie'], $deltas['BatterieLadenEnergie'],
$deltas['BatterieEntladenEnergie'] $deltas['BatterieEntladenEnergie'],
); $sdlLaden,
$sdlEntladen
) : 0.0;
return $deltas; return $deltas;
} }
@@ -2008,13 +2179,31 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return; return;
} }
$jetzt = time(); $jetzt = time();
try {
$leistungen = $this->leseMessleistungen(); $leistungen = $this->leseMessleistungen();
} catch (Throwable $fehler) {
// Keine falsche Hauslast aufzeichnen oder ueber die Messluecke integrieren.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
$this->protokolliere('Energiemessung', $fehler->getMessage());
return;
}
$letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel'); $letzterZeitstempel = $this->ReadAttributeInteger('MessungLetzterZeitstempel');
$puffer = $this->leseJsonAttribut('MessungEnergiePuffer'); $puffer = $this->leseJsonAttribut('MessungEnergiePuffer');
$energieDeltas = $this->leseEnergieZaehlerDeltas(); $dauer = $letzterZeitstempel > 0 ? max(0, $jetzt - $letzterZeitstempel) : 0;
$sdlIntervallGueltig = $letzterZeitstempel > 0 && $dauer <= 300;
$integriert = EnergieMessung::energieDeltas(
$leistungen,
$this->ReadPropertyBoolean('SDLAktiv') && !$sdlIntervallGueltig ? 0 : min(300, $dauer)
);
$energieDeltas = $this->leseEnergieZaehlerDeltas(
$integriert['SDLLadenEnergie'],
$integriert['SDLEntladenEnergie'],
!$this->ReadPropertyBoolean('SDLAktiv') || $sdlIntervallGueltig
);
if ($energieDeltas === null && $letzterZeitstempel > 0) { if ($energieDeltas === null && $letzterZeitstempel > 0) {
$sekunden = min(300, max(0, $jetzt - $letzterZeitstempel)); $energieDeltas = $integriert;
$energieDeltas = EnergieMessung::energieDeltas($leistungen, $sekunden);
} }
foreach ($energieDeltas ?? [] as $ident => $delta) { foreach ($energieDeltas ?? [] as $ident => $delta) {
$puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta; $puffer[$ident] = max(0.0, (float) ($puffer[$ident] ?? 0.0)) + $delta;
@@ -2033,10 +2222,15 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'HausverbrauchLeistungArchiv' => 'Haus', 'HausverbrauchLeistungArchiv' => 'Haus',
'NetzleistungArchiv' => 'Netz', 'NetzleistungArchiv' => 'Netz',
'BatterieleistungArchiv' => 'Batterie', 'BatterieleistungArchiv' => 'Batterie',
'SDLLeistungArchiv' => 'SDL',
] as $ident => $quelle) { ] as $ident => $quelle) {
$this->SetValue($ident, $leistungen[$quelle]); $this->SetValue($ident, $leistungen[$quelle]);
} }
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt); $this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', $jetzt);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
} }
$energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60; $energieintervall = $this->ReadPropertyInteger('EnergieaufzeichnungMinuten') * 60;
@@ -2051,10 +2245,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt); $this->WriteAttributeInteger('MessungLetzteEnergieaufzeichnung', $jetzt);
} }
$this->aktualisiereDiagrammMesswerte(true);
$funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID); $funFactsID = @IPS_GetObjectIDByIdent('EnergieFunFacts', $this->InstanceID);
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) { if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
$this->aktualisiereFunFacts($funFactsID); $this->aktualisiereFunFacts($funFactsID);
} }
$this->aktualisiereEnergieflusswerte();
} }
private function istNumerischeVariable(int $variableID): bool private function istNumerischeVariable(int $variableID): bool
@@ -2123,8 +2319,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
&& $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus); && $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
$gueltig = $leaseGueltig && $berechtigungenPassen; $gueltig = $leaseGueltig && $berechtigungenPassen;
$berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen; $berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen;
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv') $geraetezugangFehlt = $this->geraetezugangBenoetigt()
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
&& $this->ReadAttributeString('PrognoseInstallationsToken') === ''; && $this->ReadAttributeString('PrognoseInstallationsToken') === '';
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet) $routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|| $geraetezugangFehlt || $geraetezugangFehlt
@@ -2475,8 +2670,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$nutzlast = json_encode([ $nutzlast = json_encode([
'code' => $lizenzcode, 'code' => $lizenzcode,
'installationId' => $installationID, 'installationId' => $installationID,
'issueDeviceToken' => ($this->ReadPropertyBoolean('PrognoseAktiv') 'issueDeviceToken' => $this->geraetezugangBenoetigt()
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '', && $this->ReadAttributeString('PrognoseInstallationsToken') === '',
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); ], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$curl = curl_init(self::LIZENZ_ENDPOINT); $curl = curl_init(self::LIZENZ_ENDPOINT);
@@ -2550,11 +2744,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
'Prognosestatus', 'Prognosestatus',
$this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet' $this->ReadPropertyBoolean('PrognoseAktiv') ? 'Wartet' : 'NichtVerwendet'
); );
foreach (['SDLAnschluss' => 'SDLStatus'] as $property => $variable) { $this->aktualisiereSDLStatus();
$konfiguration = json_decode($this->ReadPropertyString($property), true, 512, JSON_THROW_ON_ERROR);
$aktiv = is_array($konfiguration) && ($konfiguration['Aktiv'] ?? false) === true;
$this->setzeDiagnosewert($variable, $aktiv ? 'Fehler' : 'NichtVerwendet');
}
$this->setzeDiagnosewert( $this->setzeDiagnosewert(
'Stoerueberwachungsstatus', 'Stoerueberwachungsstatus',
$this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet' $this->ReadPropertyBoolean('StoerueberwachungAktiv') ? 'Wartet' : 'NichtVerwendet'
@@ -2563,6 +2753,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
private function pruefeKonfiguration(): void private function pruefeKonfiguration(): void
{ {
$this->pruefeSDLKonfiguration();
foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) { foreach (['MesswertMaxAlter', 'KeepAlive', 'VerbraucherTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) { if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.'); throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
@@ -2579,6 +2770,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new InvalidArgumentException($property . ' darf nicht negativ sein.'); 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'); $energieLoeschen = $this->ReadPropertyInteger('EnergieLoeschenMonate');
$energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate'); $energieVerdichten = $this->ReadPropertyInteger('EnergieVerdichtenMonate');
if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) { if ($energieLoeschen > 0 && $energieLoeschen <= $energieVerdichten) {
@@ -2646,6 +2847,25 @@ class Manager extends IPSModule implements ManagerSchnittstelle
); );
} }
private function geraetezugangBenoetigt(): bool
{
if ($this->ReadPropertyBoolean('PrognoseAktiv')
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv')
) {
return true;
}
try {
$topologie = $this->leseAnlagentopologie();
} catch (Throwable $fehler) {
return false;
}
return $topologie['inverters'] !== []
|| $topologie['pvArrays'] !== []
|| $topologie['batteries'] !== [];
}
private function prognoseTelemetrieKonfiguriert(): bool private function prognoseTelemetrieKonfiguriert(): bool
{ {
return $this->ReadPropertyBoolean('PrognoseAktiv') return $this->ReadPropertyBoolean('PrognoseAktiv')
@@ -2670,38 +2890,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
return null; return null;
} }
$jetzt = time(); return ManagerRegler::interpolierteFahrplanVorgabe(
$naechsterAbstand = PHP_INT_MAX; $fahrplan['points'],
$vorgabe = null; time()
foreach ($fahrplan['points'] as $punkt) { );
if (!is_array($punkt)
|| !is_string($punkt['time'] ?? null)
|| (!is_int($punkt['gridTargetW'] ?? null)
&& !is_float($punkt['gridTargetW'] ?? null))
) {
continue;
}
$zeitpunkt = strtotime($punkt['time']);
if ($zeitpunkt === false) {
continue;
}
$abstand = abs($zeitpunkt - $jetzt);
if ($abstand < $naechsterAbstand) {
$naechsterAbstand = $abstand;
$vorgabe = (float) $punkt['gridTargetW'];
}
}
return $naechsterAbstand <= 1800 ? $vorgabe : null;
} }
private function synchronisiereAnlagentopologie(): void private function synchronisiereAnlagentopologie(): void
{ {
if (!$this->ReadPropertyBoolean('PrognoseAktiv')) {
$this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet');
return;
}
try { try {
$topologie = $this->leseAnlagentopologie(); $topologie = $this->leseAnlagentopologie();
if ($topologie['inverters'] === [] if ($topologie['inverters'] === []
@@ -2717,7 +2913,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.'); throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
} }
$this->sendeAnlagentopologie($topologie, $token); $this->sendeAnlagentopologie($topologie, $token);
$this->setzeDiagnosewert('Prognosestatus', 'Verbunden'); $this->setzeDiagnosewert(
'Prognosestatus',
$this->ReadPropertyBoolean('PrognoseAktiv')
? 'Verbunden'
: 'Topologie synchronisiert'
);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$meldung = substr($fehler->getMessage(), 0, 500); $meldung = substr($fehler->getMessage(), 0, 500);
$this->setzeDiagnosewert('Prognosestatus', 'Fehler: ' . $meldung); $this->setzeDiagnosewert('Prognosestatus', 'Fehler: ' . $meldung);
+5 -4
View File
@@ -112,6 +112,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
@@ -150,7 +151,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->regelzyklus(true); $this->regelzyklus(true);
return; return;
@@ -208,7 +209,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->regelzyklus(false); $this->regelzyklus(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -290,7 +291,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$ziel = 0; $ziel = 0;
@@ -301,7 +302,7 @@ class VerbraucherPufferspeicher extends IPSModule implements VerbraucherSchnitts
} elseif ((bool) $this->leseZustand('SollwertGueltig')) { } elseif ((bool) $this->leseZustand('SollwertGueltig')) {
$ziel = (int) $this->leseZustand('Sollleistung'); $ziel = (int) $this->leseZustand('Sollleistung');
if (!in_array($ziel, $angebot, true)) { if (!in_array($ziel, $angebot, true)) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ziel = 0; $ziel = 0;
} }
} }
+6 -5
View File
@@ -118,6 +118,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMeldungen(); $this->registriereMeldungen();
try { try {
@@ -158,7 +159,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -214,7 +215,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->planeMeldung(); $this->planeMeldung();
return; return;
@@ -254,7 +255,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
true true
) )
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$aktiv = (bool) $this->GetValue('Aktiv'); $aktiv = (bool) $this->GetValue('Aktiv');
@@ -474,7 +475,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe >= $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
@@ -528,7 +529,7 @@ class VerbraucherEinStufig extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt)); $this->WriteAttributeString('LaufzeitTag', date('Y-m-d', $jetzt));
$this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt); $this->WriteAttributeInteger('LaufzeitStandZeit', $jetzt);
$this->setzeZustand('Tageslaufzeit', 0); $this->setzeZustand('Tageslaufzeit', 0);
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->setzeZustand('Rueckmeldefehler', false); $this->setzeZustand('Rueckmeldefehler', false);
$this->WriteAttributeBoolean('Initialisiert', true); $this->WriteAttributeBoolean('Initialisiert', true);
} }
+5 -4
View File
@@ -132,6 +132,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
parent::ApplyChanges(); parent::ApplyChanges();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
$this->registriereMeldungen(); $this->registriereMeldungen();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -172,7 +173,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
case 'Aktiv': case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert); $this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) { if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
$this->aktualisiere(true); $this->aktualisiere(true);
@@ -230,7 +231,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true) && !in_array((int) $this->leseZustand('Sollleistung'), $this->leseLeistungsangebot(), true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->planeMeldung(); $this->planeMeldung();
return; return;
@@ -286,7 +287,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
$this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0); $this->WriteAttributeInteger('Laufbeginn', $laeuft ? $jetzt : 0);
$this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt); $this->schalte(WaermepumpenRegler::STEUERZUSTAND_NORMAL, $jetzt);
$steuerzustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL; $steuerzustand = WaermepumpenRegler::STEUERZUSTAND_NORMAL;
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$this->aktualisiereSperrerholung($laeuft, $jetzt); $this->aktualisiereSperrerholung($laeuft, $jetzt);
@@ -828,7 +829,7 @@ class VerbraucherWaermepumpe extends IPSModule implements VerbraucherSchnittstel
$ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit') $ablauf = $this->ReadAttributeInteger('LetzteVorgabeZeit')
+ $this->ReadPropertyInteger('VorgabeTimeout'); + $this->ReadPropertyInteger('VorgabeTimeout');
if ($this->ReadAttributeInteger('LetzteVorgabeZeit') <= 0 || $jetzt >= $ablauf) { if ($this->ReadAttributeInteger('LetzteVorgabeZeit') <= 0 || $jetzt >= $ablauf) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->SetTimerInterval('VorgabeTimeout', 0); $this->SetTimerInterval('VorgabeTimeout', 0);
} }
} }
+14
View File
@@ -130,6 +130,20 @@
"minimum": 0.1, "minimum": 0.1,
"digits": 1 "digits": 1
}, },
{
"type": "NumberSpinner",
"name": "TemperaturUntergrenze",
"caption": "Untertemperaturgrenze",
"suffix": " °C",
"digits": 1
},
{
"type": "NumberSpinner",
"name": "TemperaturObergrenze",
"caption": "Übertemperaturgrenze",
"suffix": " °C",
"digits": 1
},
{ {
"type": "List", "type": "List",
"name": "Zeitplan", "name": "Zeitplan",
+35 -14
View File
@@ -56,6 +56,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
$this->RegisterPropertyString('Zeitplan', '[]'); $this->RegisterPropertyString('Zeitplan', '[]');
$this->RegisterPropertyInteger('LastwechselSperrzeit', 5); $this->RegisterPropertyInteger('LastwechselSperrzeit', 5);
$this->RegisterPropertyInteger('TemperaturMaxAlter', 30); $this->RegisterPropertyInteger('TemperaturMaxAlter', 30);
$this->RegisterPropertyFloat('TemperaturUntergrenze', 0.0);
$this->RegisterPropertyFloat('TemperaturObergrenze', 100.0);
$this->RegisterPropertyFloat('Hysterese', 5.0); $this->RegisterPropertyFloat('Hysterese', 5.0);
$this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true); $this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true);
$this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4); $this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4);
@@ -120,6 +122,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
$this->initialisiereTemperaturen(); $this->initialisiereTemperaturen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->bereinigeUngueltigenSollwert();
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -217,7 +220,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if ((bool) $this->leseZustand('SollwertGueltig') if ((bool) $this->leseZustand('SollwertGueltig')
&& !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true) && !in_array((int) $this->leseZustand('Sollleistung'), $angebot, true)
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$this->regelzyklus(false); $this->regelzyklus(false);
} }
$this->SetTimerInterval('RueckmeldungVerzoegert', 100); $this->SetTimerInterval('RueckmeldungVerzoegert', 100);
@@ -262,7 +265,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if ($letzteVorgabe <= 0 if ($letzteVorgabe <= 0
|| $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout') || $jetzt - $letzteVorgabe > $this->ReadPropertyInteger('VorgabeTimeout')
) { ) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
} }
$ziel = 0; $ziel = 0;
@@ -274,7 +277,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
} elseif ((bool) $this->leseZustand('SollwertGueltig')) { } elseif ((bool) $this->leseZustand('SollwertGueltig')) {
$ziel = (int) $this->leseZustand('Sollleistung'); $ziel = (int) $this->leseZustand('Sollleistung');
if (!in_array($ziel, $angebot, true)) { if (!in_array($ziel, $angebot, true)) {
$this->setzeZustand('SollwertGueltig', false); $this->verwerfeSollwert();
$ziel = 0; $ziel = 0;
} }
} }
@@ -537,6 +540,10 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if (!$temperaturGueltig) { if (!$temperaturGueltig) {
$stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.'; $stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.';
} }
$untertemperatur = $this->istUntertemperatur($temperaturGueltig);
if ($untertemperatur) {
$stoerungen[] = 'Untertemperatur erkannt.';
}
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig); $uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
if ($uebertemperatur) { if ($uebertemperatur) {
$stoerungen[] = 'Uebertemperatur erkannt.'; $stoerungen[] = 'Uebertemperatur erkannt.';
@@ -555,19 +562,18 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
} }
} }
private function istUntertemperatur(bool $temperaturGueltig): bool
{
return $temperaturGueltig
&& (float) $this->GetValue('Boilertemperatur')
< $this->ReadPropertyFloat('TemperaturUntergrenze');
}
private function istUebertemperatur(bool $temperaturGueltig): bool private function istUebertemperatur(bool $temperaturGueltig): bool
{ {
if (!$temperaturGueltig) { return $temperaturGueltig
return false; && (float) $this->GetValue('Boilertemperatur')
} > $this->ReadPropertyFloat('TemperaturObergrenze');
$grenze = (float) $this->GetValue('Maximaltemperatur');
if ($this->ReadPropertyBoolean('LegionellenfunktionAktiv')) {
$grenze = max($grenze, $this->ReadAttributeFloat('ZustandLegionellentemperatur'));
}
return (float) $this->GetValue('Boilertemperatur')
> $grenze + $this->ReadPropertyFloat('Hysterese');
} }
private function sendeVerbraucherdaten(): void private function sendeVerbraucherdaten(): void
@@ -592,6 +598,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
{ {
$temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig'); $temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig');
$schaltfehler = $this->ReadAttributeString('Schaltfehler'); $schaltfehler = $this->ReadAttributeString('Schaltfehler');
$untertemperatur = $this->istUntertemperatur($temperaturGueltig);
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig); $uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
return [ return [
@@ -655,6 +662,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
'Einheit' => '', 'Einheit' => '',
'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.', 'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.',
], ],
[
'Kennung' => 'Untertemperatur',
'Art' => 'Stoerung',
'Wert' => $untertemperatur,
'Einheit' => '',
'Text' => $untertemperatur ? 'Untertemperatur erkannt.' : '',
],
[ [
'Kennung' => 'Uebertemperatur', 'Kennung' => 'Uebertemperatur',
'Art' => 'Stoerung', 'Art' => 'Stoerung',
@@ -745,6 +759,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
if ($this->ReadPropertyFloat('Hysterese') <= 0) { if ($this->ReadPropertyFloat('Hysterese') <= 0) {
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.'); throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
} }
if ($this->ReadPropertyFloat('TemperaturUntergrenze')
>= $this->ReadPropertyFloat('TemperaturObergrenze')
) {
throw new InvalidArgumentException(
'Die Temperaturuntergrenze muss kleiner als die Temperaturobergrenze sein.'
);
}
$minimum = (float) $this->GetValue('Mindesttemperatur'); $minimum = (float) $this->GetValue('Mindesttemperatur');
$maximum = (float) $this->GetValue('Maximaltemperatur'); $maximum = (float) $this->GetValue('Maximaltemperatur');
$legionellen = (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur'); $legionellen = (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur');
+23
View File
@@ -41,6 +41,29 @@ berechnet sein Leistungsangebot neu und meldet es zurueck. Eine vorhandene
Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr Sollleistung wird dabei nur verworfen, wenn sie im neuen Angebot nicht mehr
zulaessig ist. zulaessig ist.
### Verworfene Sollwerte und Anzeige
Beim Verwerfen einer gueltigen Vorgabe setzen alle Verbrauchermodule sowohl
`SollwertGueltig=false` als auch den gespeicherten und gegebenenfalls sichtbaren
Wert `Sollleistung=0`. An den Manager geht weiterhin `Sollleistung_W=null`;
die Anzeige 0 W ist keine neue gueltige Manager-Vorgabe und kein Nachweis einer
physisch ausgeschalteten Last.
Lokale Schutzprogramme, Mindestlaufzeiten und bestehende Lade-Uebergaenge
bleiben unveraendert. Berechnet ein Modul danach eine lokale Sollleistung,
darf es diese weiterhin anzeigen. Bei Ladestationen bleibt waehrend eines
Regeluebergangs insbesondere der letzte Geraetebefehl erhalten, auch wenn die
alte Manager-Vorgabe bereits verworfen und ihre Anzeige auf 0 gesetzt wurde.
Wiederholte Verwerfungen einer bereits ungueltigen Manager-Vorgabe lassen eine
inzwischen neu berechnete lokale Schutzleistung unveraendert.
Nach einem Modulupdate bereinigt `ApplyChanges` bereits gespeicherte ungueltige
Altwerte. Gueltige Vorgaben einschliesslich 0 W und negativer Batterieleistung
bleiben erhalten. Keine neuen Properties, Variablen-IDs oder Vertragsversion;
es ist keine manuelle Konfigurationsmigration erforderlich. Das Update ist
kontrolliert anzuwenden, da die regulaere Instanzinitialisierung wie bisher
Geraeteaktionen ausloesen kann.
## Verbraucher an Manager ## Verbraucher an Manager
```json ```json
+5 -3
View File
@@ -13,9 +13,11 @@
| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. | | `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. |
| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. | | `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. |
Die neuen Properties `TemperaturMaxAlter`, `Hysterese`, die Parameter der Die neuen Properties `TemperaturMaxAlter`, `TemperaturUntergrenze`,
Legionellenfunktion und `DiagnosevariablenAnzeigen` ersetzen feste Werte und `TemperaturObergrenze`, `Hysterese`, die Parameter der Legionellenfunktion und
nicht pruefbare Annahmen des Altmoduls. `DiagnosevariablenAnzeigen` ersetzen feste Werte und nicht pruefbare Annahmen
des Altmoduls. Die Stoergrenzen liegen standardmaessig bei 0 und 100 Grad C und
sind von den Regel-Sollwerten `Mindesttemperatur` und `Maximaltemperatur` getrennt.
## Variablen ## Variablen
+8 -3
View File
@@ -53,6 +53,8 @@ EinstellungenInVisu und LoggingEin stammen aus VerbraucherBasisTrait.
| ReserveLadezustand | Float | 20 | Peakshaving-Reserve in Prozent | | ReserveLadezustand | Float | 20 | Peakshaving-Reserve in Prozent |
| MindestLadezustand | Float | 10 | Untere Entladegrenze in Prozent | | MindestLadezustand | Float | 10 | Untere Entladegrenze in Prozent |
| LadezustandHysterese | Float | 2 | Hysterese oberhalb der Reserve | | 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 | | MesswertMaxAlter | Integer | 60 | Maximales Messwertalter in Sekunden |
| Aenderungssperre | Integer | 4 | Sperrzeit nach Sollwertaenderung | | Aenderungssperre | Integer | 4 | Sperrzeit nach Sollwertaenderung |
| DiagnosevariablenAnzeigen | Boolean | false | Technische Variablen einblenden | | DiagnosevariablenAnzeigen | Boolean | false | Technische Variablen einblenden |
@@ -81,9 +83,12 @@ wird.
PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder PV und Peak verwenden weiterhin die bisherige SoC-Logik. Feste Schutz- oder
Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche Peakvorgaben bleiben Einzelwerte; frei regelbare Angebote werden als Bereiche
gemeldet. Die Hysterese wird als persistenter Modulzustand gefuehrt. Bei gemeldet. Bei schutzbedingtem Nachladen innerhalb oder unterhalb der Reserve
Wechselrichtersteuerung, lokaler Deaktivierung oder ungueltigen Messwerten begrenzt MaximaleNachladeleistung die positive Angebotsgrenze, wenn
lautet das Angebot [0]. 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 ## Variablen
+11 -6
View File
@@ -50,22 +50,22 @@ als ungueltig behandelt und ergeben bis zur gueltigen 1-/3-Phasenmeldung nur
| `PrioritaetPeak` | Integer | `0` | Prioritaet im Peakbetrieb. | | `PrioritaetPeak` | Integer | `0` | Prioritaet im Peakbetrieb. |
| `Meldeintervall` | Integer | `10 s` | Periodische Vollmeldung an den Manager. | | `Meldeintervall` | Integer | `10 s` | Periodische Vollmeldung an den Manager. |
| `VorgabeTimeout` | Integer | `120 s` | Ablauf einer nicht erneuerten Manager-Vorgabe. | | `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. | | `DiagnosevariablenAnzeigen` | Boolean | `false` | Zeigt technische Diagnosewerte. |
| `LoggingEin` | Boolean | `false` | Aktiviert Debugmeldungen ohne Geheimnisse. | | `LoggingEin` | Boolean | `false` | Aktiviert Debugmeldungen ohne Geheimnisse. |
## Variablen ## Variablen
Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`, Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`,
`Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte `Ladestrom` und `Phasenzahl`. Mit `EinstellungenInVisu=true` erscheinen
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` wartet auf Start, zusaetzlich `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung`.
`3` laedt, `4` geladen oder `5` Fehler.
Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie Mit `DiagnosevariablenAnzeigen=true` werden der numerische `Fahrzeugstatus`
und die gemeinsamen EMS-Diagnosewerte sowie
`GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`, `GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
`AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`, `AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und `GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
`LeistungsangebotDiagnose`. `GrundKeinLadestrom=55` bedeutet bei Easee, dass `LeistungsangebotDiagnose` angezeigt. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
die Autorisierung fehlt. die Autorisierung fehlt.
## Regelung ## Regelung
@@ -83,6 +83,11 @@ Ladestation Stand-Alone:
Die niedrigste Grenze aus Property, Observation `47` und Observation `104` Die niedrigste Grenze aus Property, Observation `47` und Observation `104`
bestimmt den angebotenen Maximalstrom. Eine neue Observation berechnet das bestimmt den angebotenen Maximalstrom. Eine neue Observation berechnet das
Angebot sofort neu und meldet die Aenderung kurz gebuendelt an den Manager. 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 Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0` frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0`
+63 -28
View File
@@ -40,13 +40,20 @@ Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
wird entsprechend `State != 1` ausgewertet. wird entsprechend `State != 1` ausgewertet.
2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei 2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei
Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet. Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet.
3. Bei einer Leistung ueber 7500 W wird dreiphasiges Laden erkannt. Darunter 3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
wird bei verbundenem Fahrzeug einphasiges Laden angenommen. Strom fuehren, wird dreiphasiges Laden direkt erkannt.
4. Der Ladestrom wird aus der Leistung mit `230 V` einphasig beziehungsweise 4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
`1.71 * 400 V` dreiphasig berechnet. Modul zunaechst nur den Mindeststrom von 6 A als Erkennungsstrom vor. Bis zur
5. War das Fahrzeug bereits im vorherigen Zyklus verbunden und liegt der abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot `[0]`.
ermittelte Maximalstrom aus Ladestrom plus `2.5 A` unter `6 A`, wird 5. Nach `Phasenerkennungszeit` wird die gemessene Leistung relativ zum
`FahrzeugGeladen=true` gesetzt. 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.
7. Ein Ladeende wird nur nach zuvor tatsaechlich gemessener Ladeleistung
erkannt. Eine vom EMS angeordnete Solarpause gilt deshalb nicht als
`FahrzeugGeladen`.
Der normalisierte `Fahrzeugstatus` verwendet folgende Werte: Der normalisierte `Fahrzeugstatus` verwendet folgende Werte:
@@ -72,10 +79,24 @@ stoppt die Ladestation.
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt | | Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
| Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum | | Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
Die festen ein- und dreiphasigen Leistungsstufen sowie deren Rundung wurden aus Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
Enelix 1 uebernommen. Ohne gueltige Manager-Vorgabe faehrt das Modul bei `684 W` pro Ampere dreiphasig. Ohne gueltige Manager-Vorgabe faehrt das Modul
aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es die bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
maximale angebotene Leistung. 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`
wird jedoch nicht angeboten. Nach dem Abschalten meldet das Modul waehrend der
`Mindestausschaltdauer` ausschliesslich `[0]`. Sicherheitsabschaltungen
durch Deaktivierung, fehlende Ladefreigabe, Ausstecken oder ein erkanntes
Ladeende greifen sofort.
## Bedienung ## Bedienung
@@ -100,7 +121,7 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
| `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. | | `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. |
| `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. | | `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. |
| `VorgabeTimeout` | Integer | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe ohne Erneuerung. | | `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. | | `LoggingEin` | Boolean | `false` | Aktiviert das laufende Debug-Protokoll des Moduls. |
### Ladestation und Diagnose ### Ladestation und Diagnose
@@ -114,8 +135,13 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. | | `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. | | `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. | | `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
| `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. |
| `Mindestausschaltdauer` | Integer | `0 min` | Mindestpause nach dem Abschalten, in der nur 0 W angeboten werden. |
| `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. | | `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. |
| `Ladefreigabe` | Boolean | `false` | Startwert der lokalen Ladefreigabe. | | `Ladefreigabe` | Boolean | `true` | Startwert der lokalen Ladefreigabe. |
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. | | `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. | | `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. |
@@ -132,28 +158,28 @@ Immer sichtbar:
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. | | `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. |
| `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. | | `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. |
| `FahrzeugGeladen` | Boolean / Anzeige | Ergebnis der kompatiblen Enelix-1-Voll-Erkennung. | | `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. | | `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Ladestrom in A. |
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig oder `3` dreiphasig. | | `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` - `Ladefreigabe`
- `Solarladen` - `Solarladen`
- `Istleistung`
- `Sollleistung`
Nur mit `DiagnosevariablenAnzeigen=true` sichtbar: Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
| Ident | Beschreibung | | Ident | Beschreibung |
| --- | --- | | --- | --- |
| `Istleistung` | Von der Ladestation gemessene Leistung in W. |
| `Leistungsquelle` | Immer `2` fuer eine gemessene Leistung. | | `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. | | `SollwertGueltig` | Zeigt, ob die Vorgabe noch gueltig und im aktuellen Angebot enthalten ist. |
| `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. | | `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. |
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. | | `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
| `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. | | `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. |
| `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. | | `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. |
| `LetzterGeraetebefehl` | Letzte HTTP-Methode und URL ohne Zugangsdaten. | | `LetzterGeraetebefehl` | Letzter Steuerbefehl mit HTTP-Methode und URL ohne Zugangsdaten; Statusabfragen ueberschreiben ihn nicht. |
| `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. | | `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. |
Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der
@@ -232,16 +258,22 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
- Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`. - Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`.
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das - Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
Leistungsangebot `[0]`. Leistungsangebot `[0]`.
- Nach dem Anstecken wird das Fahrzeug erkannt und die gemessene Leistung - Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
plausibel in `Istleistung` und `Ladestrom` abgebildet. zunaechst der Mindeststrom von 6 A zur Erkennung angefordert.
- Bei einer Ladeleistung bis 7500 W wird eine Phase, darueber werden drei - Nach der Erkennungszeit werden eine oder drei Phasen relativ zum Pruefstrom
Phasen angezeigt. gespeichert. Bei go-e koennen die einzelnen Phasenstroeme die Erkennung
bereits vorher abschliessen.
- Die erkannte Phasenzahl bleibt bei einer anschliessenden Ladepause stabil.
- Die gemessene Leistung wird plausibel in `Istleistung` und `Ladestrom`
abgebildet.
- `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation. - `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation.
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an. - PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an.
- PV mit `Solarladen=false` bietet nur die maximale Leistung an. - PV mit `Solarladen=false` bietet nur die maximale Leistung an.
- Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an. - Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an.
- Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an. - Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an.
- Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen. - Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen.
- Leistungswechsel-, Mindest-Ein- und Mindest-Aus-Zeiten werden im gemeldeten
Angebot sichtbar und vom Manager eingehalten.
- Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten. - Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten.
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine - Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
Zugangsdaten. Zugangsdaten.
@@ -256,9 +288,11 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der - Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
`ActiveChargingPower`. `ActiveChargingPower`.
- Falsche Phasenzahl bei sehr kleiner oder stehender Ladung: Die kompatible - Phasenzahl bleibt laenger auf `0`: Waehrend der Erkennungsphase muessen
Enelix-1-Erkennung basiert auf der momentanen Leistung. Fuer eine sichere Ladestation und Fahrzeug den angeforderten Maximalstrom tatsaechlich
Dreiphasenerkennung muss waehrend der Erkennung mehr als 7500 W anliegen. freigeben. Bei Pico muss fuer eine sichere Dreiphasenerkennung waehrend der
Erkennung mehr als 7500 W anliegen; go-e kann zusaetzlich anhand der
einzelnen Phasenstroeme erkennen.
- Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie - Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren. `LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
@@ -270,9 +304,10 @@ URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
geprueft. geprueft.
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
API-Antworten. Er prueft alle drei Geraetevarianten, Fahrzeug- und API-Antworten. Er prueft alle drei Geraetevarianten, die Mindeststrom-Probe
Phasenerkennung, die Leistungsangebote in PV und Peak sowie die Umschaltung beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
ueber den `Solarladen`-Button. Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
Umschaltung ueber den `Solarladen`-Button.
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys: PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
+89 -6
View File
@@ -73,6 +73,12 @@ Die drei Regelungsvariablen `Aktiv`, `Betriebsart` und `Netzleistung` sind immer
| `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. | | `PrognoseSendeintervall` | Integer | `60` s; Intervall fuer den Upload aktueller Messwerte, zulaessig sind 60 bis 3600 Sekunden. |
| `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. | | `PrognoseAnschluss` | String/JSON | Verdeckte Altproperty fuer bestehende Konfigurationen. |
| `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. | | `SDLAnschluss` | String/JSON | Optionaler SDL/VGT-Anschluss. |
| `SDLAktiv` | Boolean | `false`; separat gemessene SDL-/Regelenergie in der Anlagenbilanz beruecksichtigen. Keine SDL-Steuerung. |
| `SDLLeistungVariableID` | Integer | `0`; bei aktiver SDL erforderliche numerische Istleistungsquelle. |
| `SDLLeistungsfaktor` | Float | `1`; Umrechnung nach W, positiv Laden/Bezug, negativ Entladen/Abgabe. Fuer kW `1000`, bei umgekehrtem Vorzeichen negativ. |
| `SDLSOCVariableID` | Integer | `0`; optionaler separater SDL-Ladezustand in Prozent, 0 bis 100. |
| `SDLEnergieflussAnzeigen` | Boolean | `false`; SDL mit optionalem SOC als eigenen Speicher im aktivierten Energiefluss anzeigen. |
| `SDLDiagrammeAnzeigen` | Boolean | `false`; SDL-Leistung, Lade-/Entladeenergie und optionalen SOC in aktivierten Diagrammen anzeigen. |
| `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. | | `Lizenzcode` | String | Im Enelix-Lizenzportal erworbener Aktivierungscode. |
| `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. | | `StoermeldeAnschluss` | String/JSON | Optionaler Anschluss zur Störüberwachung. |
| `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. | | `DiagnosevariablenAnzeigen` | Boolean | `false`; zusätzliche Diagnosevariablen anlegen oder entfernen. |
@@ -134,6 +140,59 @@ entfernt beziehungsweise die native Energy Distribution ausgeblendet:
- responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten, - responsive Energy Facts mit Solar-, Netz-, Haus- und Vergleichswerten,
- native Energy Distribution fuer PV, Netz, Haus und Batterie. - native Energy Distribution fuer PV, Netz, Haus und Batterie.
## SDL / Regelenergie
`SDL aktiv` blendet die Quellen- und Anzeigeauswahl ein. Die SDL-Istleistung
beschreibt einen **separat gemessenen AC-Zweig am selben Netzanschlusspunkt**.
Sie darf weder in der normalen Batterieleistung noch in einer PV-Leistung
enthalten sein. Eine identische Variablen-ID fuer SDL und Netz/PV/Batterie wird
abgewiesen. Ueberschneidungen in extern gebildeten Summen muss die
Anlagenkonfiguration ausschliessen. Ein gemeinsamer Speicher mit nur einem
Gesamtleistungsmesswert kann damit nicht in EMS- und SDL-Anteile zerlegt werden.
Bei aktiver SDL lautet die Hauslast `max(0, PV + Netz - Batterie - SDL)`.
Die Korrektur geschieht vor der Begrenzung auf null und unabhaengig davon,
ob SDL angezeigt wird. Diese Hauslast wird an den Prognosedienst gesendet;
Netzleistung und der kapazitaetsgewichtete SOC der normalen Batterien bleiben
unveraendert. SDL-SOC ersetzt keinen Batterie-SOC und wird nicht als
planbare EMS-Speicherkapazitaet exportiert. Ungueltige SDL-Leistungswerte
unterbrechen die Messaufzeichnung und den jeweiligen Telemetrieversand,
statt als null in die Hauslast einzugehen. Der SDL-Status ist bei aktiver
SDL auch ohne eingeschaltete Diagnosevariablen sichtbar.
`SDLLeistungArchiv` zeichnet Watt auf, `SDLSOCArchiv` gueltige Prozentwerte.
`SDLLadenEnergie` und `SDLEntladenEnergie` integrieren die getrennten Richtungen
in kWh. Die Abtastung erfolgt wie bei den anderen Leistungen minuetlich, nicht
als abrechnungsgenaue SDL-Energiemessung. Auch bei vorhandenen physikalischen
Energiezaehlern wird die Hausenergie um die im selben Intervall integrierte
SDL-Ladung vermindert und um SDL-Entladung erhoeht. Bei Messluecken ueber
fuenf Minuten, Quellenwechseln oder Zaehlerresets wird keine unvollstaendige
Hausenergie nachberechnet; Historie und bereits erfasste Zaehler bleiben erhalten.
Die Anzeigeoptionen wirken beim Anwenden direkt auf die vorhandenen
Manager-Visualisierungen. Nur die eigenen SDL-Knoten und -Datensaetze werden
ergaenzt oder entfernt. Andere Knoten, Farben, Datensaetze und sonstige
Anpassungen bleiben bestehen. Im Energiefluss verwendet SDL dieselbe W/kW-
Einheit wie die anderen Manager-Knoten; ein ungueltiger SOC wird dort nicht
als aktueller Zusatzwert angezeigt. Ohne separaten SDL-Knoten enthaelt der
Manager-Sammelknoten Batteriespeicher die normale Batterie plus SDL, damit
die dargestellte Leistungsbilanz vollstaendig bleibt. Im Leistungsdiagramm
erscheint die signierte SDL-Istleistung, im Energiediagramm erscheinen Laden
und Entladen getrennt. SOC verwendet in beiden Diagrammen eine eigene rechte
Prozentachse. Die allgemeinen Schalter fuer Energiefluss, Diagramme und
Energieaufzeichnung bleiben erforderlich.
Migration: SDL bleibt nach dem Modulupdate standardmaessig aus. Bestehende
Installationen behalten damit ihre bisherige Bilanz. Erst Quellen und
Vorzeichen pruefen, SDL aktivieren und anwenden. Historische Hauslasten oder
bereits trainierte Prognosen werden nicht rueckwirkend korrigiert; neue
Prognoseeingaben sind ab Aktivierung bereinigt. Ausschalten entfernt nur
SDL-Anzeigen, nicht die aufgezeichnete Historie. Der alte `SDLAnschluss`
bleibt lesbar; externe SDL-Regelung und Stellbefehle werden nicht veraendert.
Native Darstellungsvertraege: [Energy Distribution](https://www.symcon.de/en/service/documentation/module-reference/energy/energy-distribution/)
und [Diagramme](https://www.symcon.de/en/service/documentation/basics/media/charts/).
## Anlagenweite Einspeisebegrenzung ## Anlagenweite Einspeisebegrenzung
Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der Die Einspeisebegrenzung ist Bestandteil von Peak Shaving und arbeitet auf der
@@ -310,12 +369,36 @@ Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
wird mit genau `11000 W` zugeteilt. wird mit genau `11000 W` zugeteilt.
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
und danach stabil nach Instanz-ID sortiert. Zuerst reserviert der Manager für sortiert. Zuerst reserviert der Manager die richtungsneutralen erlaubten Werte
jeden Verbraucher den kleinsten erlaubten Leistungswert. Nur das danach sowie die gemessene Leistung fester oder gesperrter Verbraucher.
verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch Positives Restbudget wird innerhalb jeder Priorität schrittweise verteilt:
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im Zuerst kommt die kleinste nächste erreichbare **absolute Sollleistung**, nicht
`Verbraucherstatus` dokumentiert. 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 Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
+10 -5
View File
@@ -52,7 +52,8 @@ final class BatterieRegler
float $netzleistungW, float $netzleistungW,
float $istleistungW, float $istleistungW,
float $maxLadenW, float $maxLadenW,
float $maxEntladenW float $maxEntladenW,
?float $maxNachladenW = null
): array { ): array {
self::pruefeLadezustandsgrenzen($reserve, 0.0); self::pruefeLadezustandsgrenzen($reserve, 0.0);
if ($minimum < 0.0 || $minimum > 100.0 || $minimum > $reserve) { if ($minimum < 0.0 || $minimum > 100.0 || $minimum > $reserve) {
@@ -61,12 +62,16 @@ final class BatterieRegler
); );
} }
self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW); self::pruefeLeistungsgrenzen($maxLadenW, $maxEntladenW);
if ($maxNachladenW !== null && $maxNachladenW < 0.0) {
throw new InvalidArgumentException('Maximale Nachladeleistung darf nicht negativ sein.');
}
if (!$aktiv || !$enelixSteuert) { if (!$aktiv || !$enelixSteuert) {
return [0]; return [0];
} }
$maxLaden = (int) floor($maxLadenW); $maxLaden = (int) floor($maxLadenW);
$maxNachladen = (int) floor(min($maxLadenW, $maxNachladenW ?? $maxLadenW));
$maxEntladen = (int) floor($maxEntladenW); $maxEntladen = (int) floor($maxEntladenW);
if ($peakbetrieb) { if ($peakbetrieb) {
if ($ladezustand > $reserve && !$hystereseAktiv) { if ($ladezustand > $reserve && !$hystereseAktiv) {
@@ -80,10 +85,10 @@ final class BatterieRegler
return self::leistungsbereich(-$maxEntladen, 0); return self::leistungsbereich(-$maxEntladen, 0);
} }
if ($ladezustand > $minimum) { 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) { if ($ladezustand > 99.0) {
@@ -93,10 +98,10 @@ final class BatterieRegler
return self::leistungsbereich(-$maxEntladen, $maxLaden); return self::leistungsbereich(-$maxEntladen, $maxLaden);
} }
if ($ladezustand >= $reserve && $hystereseAktiv) { if ($ladezustand >= $reserve && $hystereseAktiv) {
return self::leistungsbereich(0, $maxLaden); return self::leistungsbereich(0, $maxNachladen);
} }
return [$maxLaden]; return [$maxNachladen];
} }
/** /**
+6 -1
View File
@@ -53,13 +53,18 @@ final class EaseeLadestatus
? self::phasenzahl((int) ($beobachtungen['110'] ?? 0)) ? self::phasenzahl((int) ($beobachtungen['110'] ?? 0))
: 0; : 0;
$leistung = max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0); $laedt = $betriebszustand === 3;
$leistung = $laedt
? max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0)
: 0.0;
$stroeme = []; $stroeme = [];
if ($laedt) {
foreach ([182, 183, 184, 185] as $id) { foreach ([182, 183, 184, 185] as $id) {
if (array_key_exists((string) $id, $beobachtungen)) { if (array_key_exists((string) $id, $beobachtungen)) {
$stroeme[] = abs((float) $beobachtungen[(string) $id]); $stroeme[] = abs((float) $beobachtungen[(string) $id]);
} }
} }
}
$ladestrom = $stroeme === [] ? 0.0 : max($stroeme); $ladestrom = $stroeme === [] ? 0.0 : max($stroeme);
$grenzen = [$konfigurierterMaximalstrom]; $grenzen = [$konfigurierterMaximalstrom];
+14 -5
View File
@@ -8,11 +8,11 @@ final class EnergieMessung
{ {
private const WATTSEKUNDEN_PRO_KWH = 3600000.0; private const WATTSEKUNDEN_PRO_KWH = 3600000.0;
public static function hausleistung(float $pv, float $netz, float $batterie): float public static function hausleistung(float $pv, float $netz, float $batterie, float $sdl = 0.0): float
{ {
return max( return max(
0.0, 0.0,
self::endlich($pv) + self::endlich($netz) - self::endlich($batterie) self::endlich($pv) + self::endlich($netz) - self::endlich($batterie) - self::endlich($sdl)
); );
} }
@@ -21,7 +21,9 @@ final class EnergieMessung
float $netzbezug, float $netzbezug,
float $netzeinspeisung, float $netzeinspeisung,
float $batterieLaden, float $batterieLaden,
float $batterieEntladen float $batterieEntladen,
float $sdlLaden = 0.0,
float $sdlEntladen = 0.0
): float { ): float {
return max( return max(
0.0, 0.0,
@@ -30,18 +32,22 @@ final class EnergieMessung
+ self::endlich($batterieEntladen) + self::endlich($batterieEntladen)
- self::endlich($netzeinspeisung) - self::endlich($netzeinspeisung)
- self::endlich($batterieLaden) - self::endlich($batterieLaden)
- self::endlich($sdlLaden)
+ self::endlich($sdlEntladen)
); );
} }
/** /**
* @param array{PV: float, Haus: float, Netz: float, Batterie: float} $leistungen * @param array{PV: float, Haus: float, Netz: float, Batterie: float, SDL?: float} $leistungen
* @return array{ * @return array{
* PVEnergie: float, * PVEnergie: float,
* Hausenergie: float, * Hausenergie: float,
* NetzbezugEnergie: float, * NetzbezugEnergie: float,
* EinspeisungEnergie: float, * EinspeisungEnergie: float,
* BatterieLadenEnergie: float, * BatterieLadenEnergie: float,
* BatterieEntladenEnergie: float * BatterieEntladenEnergie: float,
* SDLLadenEnergie: float,
* SDLEntladenEnergie: float
* } * }
*/ */
public static function energieDeltas(array $leistungen, int $sekunden): array public static function energieDeltas(array $leistungen, int $sekunden): array
@@ -51,6 +57,7 @@ final class EnergieMessung
$haus = self::endlich($leistungen['Haus']); $haus = self::endlich($leistungen['Haus']);
$netz = self::endlich($leistungen['Netz']); $netz = self::endlich($leistungen['Netz']);
$batterie = self::endlich($leistungen['Batterie']); $batterie = self::endlich($leistungen['Batterie']);
$sdl = self::endlich($leistungen['SDL'] ?? 0.0);
return [ return [
'PVEnergie' => max(0.0, $pv) * $faktor, 'PVEnergie' => max(0.0, $pv) * $faktor,
@@ -59,6 +66,8 @@ final class EnergieMessung
'EinspeisungEnergie' => max(0.0, -$netz) * $faktor, 'EinspeisungEnergie' => max(0.0, -$netz) * $faktor,
'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor, 'BatterieLadenEnergie' => max(0.0, $batterie) * $faktor,
'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor, 'BatterieEntladenEnergie' => max(0.0, -$batterie) * $faktor,
'SDLLadenEnergie' => max(0.0, $sdl) * $faktor,
'SDLEntladenEnergie' => max(0.0, -$sdl) * $faktor,
]; ];
} }
+40 -5
View File
@@ -91,7 +91,7 @@ final class LadestationAdapter
} }
/** /**
* @return array{Rohstatus: int, Istleistung_W: float} * @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 public static function dekodiereStatus(int $geraetetyp, string $antwort): array
{ {
@@ -110,12 +110,47 @@ final class LadestationAdapter
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.'); throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
} }
$faktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0; $leistungsfaktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
$status = [
return [
'Rohstatus' => (int) $daten['car'], 'Rohstatus' => (int) $daten['car'],
'Istleistung_W' => (float) $daten['nrg'][11] * $faktor, '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])
&& is_numeric($daten['nrg'][6])
) {
$stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0;
$phasenstroeme = [
(float) $daten['nrg'][4] * $stromfaktor,
(float) $daten['nrg'][5] * $stromfaktor,
(float) $daten['nrg'][6] * $stromfaktor,
];
$status['Iststrom_A'] = max($phasenstroeme);
$aktivePhasen = count(array_filter(
$phasenstroeme,
static fn (float $strom): bool => $strom >= 1.0
));
if ($aktivePhasen > 0) {
$status['Phasenzahl'] = $aktivePhasen >= 2 ? 3 : 1;
}
}
return $status;
} }
if ($geraetetyp === self::SMART_ME_PICO) { if ($geraetetyp === self::SMART_ME_PICO) {
+81 -27
View File
@@ -12,33 +12,40 @@ final class LadestationRegler
public const PHASEN_EIN = 1; public const PHASEN_EIN = 1;
public const PHASEN_DREI = 3; public const PHASEN_DREI = 3;
/** @var list<int> */ private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
private const LEISTUNGEN_EINPHASIG = [ private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
1450, 1690, 1930, 2170, 2410, 2650, 2890, 3130, 3370,
3610, 3850, 4090, 4330, 4570, 4810, 5050, 5290, 5530,
5770, 6010, 6250, 6490, 6730, 6970, 7210, 7450, 7690,
];
/** @var list<int> */ public static function erkennePhasenzahlAusLeistung(
private const LEISTUNGEN_DREIPHASIG = [ float $leistung,
4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700, int $angeforderterStrom
10300, 11000, 11750, 12450, 13150, 13850, 14550, 15250, ): int {
15900, 16600, 17300, 18000, 18700, 19400, 20100, 20800, if (!is_finite($leistung) || $leistung < 0.0) {
21500, 22000, 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, die Voll-Erkennung und der Zuschlag von 2.5 A stammen * Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
* aus der Enelix-1-Ladestation und bleiben bewusst kompatibel. * Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
* die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt.
* *
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus * @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $rohstatus
* @return array<string, int|float|bool> * @return array<string, int|float|bool>
*/ */
public static function normalisiereStatus( public static function normalisiereStatus(
array $rohstatus, array $rohstatus,
bool $warVerbunden, bool $warVerbunden,
bool $warGeladen = false, bool $warGeladen = false,
bool $ladungErwartet = true bool $ladungErwartet = true,
bool $ladeleistungWurdeErkannt = false,
int $bekanntePhasenzahl = self::PHASEN_UNBEKANNT
): array { ): array {
$verbunden = $rohstatus['Rohstatus'] !== 1; $verbunden = $rohstatus['Rohstatus'] !== 1;
$leistung = max(0.0, $rohstatus['Istleistung_W']); $leistung = max(0.0, $rohstatus['Istleistung_W']);
@@ -47,12 +54,28 @@ final class LadestationRegler
$geladen = false; $geladen = false;
if ($verbunden) { if ($verbunden) {
$phasen = $leistung > 7500.0 ? self::PHASEN_DREI : self::PHASEN_EIN; if (in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)) {
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0; $phasen = $bekanntePhasenzahl;
$ladestrom = $leistung / $nenner; } elseif (isset($rohstatus['Phasenzahl'])
&& in_array($rohstatus['Phasenzahl'], [self::PHASEN_EIN, self::PHASEN_DREI], true)
) {
$phasen = (int) $rohstatus['Phasenzahl'];
} elseif ($leistung > 7500.0) {
$phasen = self::PHASEN_DREI;
}
$nenner = $phasen === self::PHASEN_DREI
? self::LEISTUNG_PRO_AMPERE_DREIPHASIG
: self::LEISTUNG_PRO_AMPERE_EINPHASIG;
$ladestrom = isset($rohstatus['Iststrom_A'])
? max(0.0, (float) $rohstatus['Iststrom_A'])
: ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
$ermittelterMaximalstrom = 2.5 + $ladestrom; $ermittelterMaximalstrom = 2.5 + $ladestrom;
$geladen = $warGeladen $geladen = $warGeladen
|| ($warVerbunden && $ladungErwartet && $ermittelterMaximalstrom < 6.0); || ($warVerbunden
&& $ladungErwartet
&& $ladeleistungWurdeErkannt
&& $ermittelterMaximalstrom < 6.0);
} }
if (!$verbunden) { if (!$verbunden) {
@@ -75,6 +98,29 @@ final class LadestationRegler
]; ];
} }
public static function ermittleFahrzeugMaximalstrom(
int $vorgegebenerStrom,
float $gemessenerStrom,
int $konfigurierterMaximalstrom
): ?int {
if ($konfigurierterMaximalstrom < 6 || $konfigurierterMaximalstrom > 32) {
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
}
if ($vorgegebenerStrom < 6 || $gemessenerStrom < 5.5) {
return null;
}
$kandidat = max(
6,
min($konfigurierterMaximalstrom, (int) floor($gemessenerStrom + 2.5))
);
if ($kandidat > $vorgegebenerStrom - 2 || $kandidat >= $konfigurierterMaximalstrom) {
return null;
}
return $kandidat;
}
/** @return list<int> */ /** @return list<int> */
public static function leistungsangebot( public static function leistungsangebot(
bool $aktiv, bool $aktiv,
@@ -90,6 +136,10 @@ final class LadestationRegler
return [0]; return [0];
} }
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
return [0];
}
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom); $stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
if ($stufen === []) { if ($stufen === []) {
return [0]; return [0];
@@ -140,12 +190,16 @@ final class LadestationRegler
private static function stufenFuerPhasen(int $phasenzahl): array private static function stufenFuerPhasen(int $phasenzahl): array
{ {
if ($phasenzahl === self::PHASEN_EIN) { if ($phasenzahl === self::PHASEN_EIN) {
return self::LEISTUNGEN_EINPHASIG; $leistungProAmpere = self::LEISTUNG_PRO_AMPERE_EINPHASIG;
} } elseif ($phasenzahl === self::PHASEN_DREI) {
if ($phasenzahl === self::PHASEN_DREI) { $leistungProAmpere = self::LEISTUNG_PRO_AMPERE_DREIPHASIG;
return self::LEISTUNGEN_DREIPHASIG; } else {
}
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.'); throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
} }
return array_map(
static fn (int $strom): int => $strom * $leistungProAmpere,
range(6, 32)
);
}
} }
+820 -35
View File
@@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS; namespace Belevo\EnelixEMS;
use RuntimeException;
use Throwable;
trait ManagerEnergieTrait trait ManagerEnergieTrait
{ {
private function konfiguriereEnergiearchiv(): void private function konfiguriereEnergiearchiv(): void
@@ -23,9 +26,13 @@ trait ManagerEnergieTrait
); );
foreach (self::LEISTUNGS_VARIABLEN as $ident) { foreach (self::LEISTUNGS_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident); $variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv); $loggen = $aktiv && (!str_starts_with($ident, 'SDL') || $this->ReadPropertyBoolean('SDLAktiv'))
&& ($ident !== 'SDLSOCArchiv' || $this->ReadPropertyInteger('SDLSOCVariableID') > 0);
if (!$this->setzeArchivLogging($archivID, $variableID, $loggen)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 0); AC_SetAggregationType($archivID, $variableID, 0);
AC_SetGraphStatus($archivID, $variableID, $aktiv); AC_SetGraphStatus($archivID, $variableID, $loggen);
$regeln = [-1 => $leistungstyp]; $regeln = [-1 => $leistungstyp];
$loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate'); $loeschen = $this->ReadPropertyInteger('LeistungLoeschenMonate');
if ($loeschen > 0) { if ($loeschen > 0) {
@@ -38,7 +45,9 @@ trait ManagerEnergieTrait
foreach (self::ENERGIE_VARIABLEN as $ident) { foreach (self::ENERGIE_VARIABLEN as $ident) {
$variableID = $this->GetIDForIdent($ident); $variableID = $this->GetIDForIdent($ident);
AC_SetLoggingStatus($archivID, $variableID, $aktiv); if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
AC_SetAggregationType($archivID, $variableID, 1); AC_SetAggregationType($archivID, $variableID, 1);
AC_SetGraphStatus($archivID, $variableID, $aktiv); AC_SetGraphStatus($archivID, $variableID, $aktiv);
$regeln = [-1 => $energietyp]; $regeln = [-1 => $energietyp];
@@ -55,12 +64,57 @@ 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_');
if (!$this->setzeArchivLogging($archivID, $variableID, $aktiv)) {
continue;
}
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); IPS_ApplyChanges($archivID);
foreach (array_unique($geaendert) as $variableID) { foreach (array_unique($geaendert) as $variableID) {
AC_ReAggregateVariable($archivID, $variableID); AC_ReAggregateVariable($archivID, $variableID);
} }
} }
private function setzeArchivLogging(int $archivID, int $variableID, bool $aktiv): bool
{
// Archiv-Metadaten sind nur bei aktivierter Aufzeichnung aenderbar.
if (!$aktiv && AC_GetLoggingStatus($archivID, $variableID)) {
AC_SetGraphStatus($archivID, $variableID, false);
}
AC_SetLoggingStatus($archivID, $variableID, $aktiv);
return $aktiv;
}
/** @param array<int, int> $regeln */ /** @param array<int, int> $regeln */
private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool private function setzeVerdichtung(int $archivID, int $variableID, array $regeln): bool
{ {
@@ -129,6 +183,7 @@ trait ManagerEnergieTrait
} }
$this->entferneDiagrammUnterobjekt('Leistungsdiagramm'); $this->entferneDiagrammUnterobjekt('Leistungsdiagramm');
$this->entferneDiagrammUnterobjekt('Energiediagramm'); $this->entferneDiagrammUnterobjekt('Energiediagramm');
$this->WriteAttributeString('EnergiediagrammKonfiguration', '{}');
} }
private function synchronisiereFunFacts(bool $anzeigen): void private function synchronisiereFunFacts(bool $anzeigen): void
@@ -154,9 +209,7 @@ trait ManagerEnergieTrait
return; return;
} }
$instanzID = $this->findeUnterobjekt('Energiefluss'); $instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) { if ($instanzID !== 0) {
return;
}
$objekt = IPS_GetObject($instanzID); $objekt = IPS_GetObject($instanzID);
$modulID = $objekt['ObjectType'] === 1 $modulID = $objekt['ObjectType'] === 1
? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID'] ? (string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
@@ -166,6 +219,9 @@ trait ManagerEnergieTrait
} }
IPS_SetHidden($instanzID, true); IPS_SetHidden($instanzID, true);
} }
$this->WriteAttributeString('EnergieflussKonfiguration', '{}');
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', true);
}
private function entferneInstanzUnterobjekt(string $ident, string $modulID): void private function entferneInstanzUnterobjekt(string $ident, string $modulID): void
{ {
@@ -216,6 +272,9 @@ trait ManagerEnergieTrait
if ($modulID !== self::ENERGY_PIE_MODULE_ID) { if ($modulID !== self::ENERGY_PIE_MODULE_ID) {
throw new RuntimeException('Unterobjekt EnergyPie hat einen unpassenden Typ.'); 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_SetName($instanzID, 'Energy Pie');
IPS_SetPosition($instanzID, 500); IPS_SetPosition($instanzID, 500);
@@ -239,6 +298,7 @@ trait ManagerEnergieTrait
if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) { if (!in_array(self::ENERGY_FLOW_MODULE_ID, IPS_GetModuleList(), true)) {
throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.'); throw new RuntimeException('Das Symcon-Modul Energy Distribution ist nicht installiert.');
} }
$neuErzeugen = $this->ReadAttributeBoolean('EnergieflussNeuErzeugen');
$instanzID = $this->findeUnterobjekt('Energiefluss'); $instanzID = $this->findeUnterobjekt('Energiefluss');
if ($instanzID === 0) { if ($instanzID === 0) {
$instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID); $instanzID = IPS_CreateInstance(self::ENERGY_FLOW_MODULE_ID);
@@ -252,16 +312,81 @@ trait ManagerEnergieTrait
if ($modulID !== self::ENERGY_FLOW_MODULE_ID) { if ($modulID !== self::ENERGY_FLOW_MODULE_ID) {
throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.'); throw new RuntimeException('Unterobjekt Energiefluss hat einen unpassenden Typ.');
} }
if (!$neuErzeugen) {
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
$this->synchronisiereSDLEnergiefluss($instanzID);
IPS_SetHidden($instanzID, false);
$this->aktualisiereEnergieflusswerte();
return 'Bestehende Energiefluss-Konfiguration bleibt unveraendert.';
} }
}
$this->visualisierungsKonfiguration('EnergieflussKonfiguration');
IPS_SetName($instanzID, 'Energiefluss'); IPS_SetName($instanzID, 'Energiefluss');
IPS_SetPosition($instanzID, 540); IPS_SetPosition($instanzID, 540);
IPS_SetHidden($instanzID, false); IPS_SetHidden($instanzID, false);
$profil = $this->energieflussLeistungsprofil();
$pvID = $this->energieflussLeistungsvariable(
'EnergieflussPV',
'Energiefluss PV',
$profil,
600
);
$netzID = $this->energieflussLeistungsvariable(
'EnergieflussNetz',
'Energiefluss Netz',
$profil,
610
);
$hausID = $this->energieflussLeistungsvariable(
'EnergieflussHaus',
'Energiefluss Hausverbrauch',
$profil,
620
);
$batterieID = $this->energieflussLeistungsvariable(
'EnergieflussBatterie',
'Energiefluss Batteriespeicher',
$profil,
630
);
$verbraucher = $this->energieflussVerbraucher();
$knoten = [ $knoten = [
$this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'), $this->energieflussKnoten($pvID, 'PV-Anlage', 'solar'),
$this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'), $this->energieflussKnoten($netzID, 'Stromnetz', 'grid'),
$this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'), $this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'),
$this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'),
]; ];
$position = 640;
foreach ($verbraucher as $eintrag) {
$istID = $this->energieflussLeistungsvariable(
'EnergieflussIst_' . $eintrag['InstanzID'],
$eintrag['Name'] . ' Istleistung',
$profil,
$position
);
$sollID = $this->energieflussLeistungsvariable(
'EnergieflussSoll_' . $eintrag['InstanzID'],
$eintrag['Name'] . ' Sollleistung',
$profil,
$position + 1
);
$knoten[] = $this->energieflussKnoten(
$istID,
$eintrag['Name'],
$eintrag['Typ'],
true,
$sollID
);
$position += 10;
}
$knoten[] = $this->energieflussKnoten(
$batterieID,
'Batteriespeicher',
'storage'
);
IPS_SetProperty($instanzID, 'Calculation', true); IPS_SetProperty($instanzID, 'Calculation', true);
IPS_SetProperty( IPS_SetProperty(
$instanzID, $instanzID,
@@ -271,47 +396,700 @@ trait ManagerEnergieTrait
IPS_SetProperty($instanzID, 'EnableLimits', false); IPS_SetProperty($instanzID, 'EnableLimits', false);
IPS_SetProperty($instanzID, 'Restrictions', '[]'); IPS_SetProperty($instanzID, 'Restrictions', '[]');
IPS_ApplyChanges($instanzID); IPS_ApplyChanges($instanzID);
$this->WriteAttributeBoolean('EnergieflussNeuErzeugen', false);
$this->synchronisiereSDLEnergiefluss($instanzID, true);
$this->aktualisiereEnergieflusswerte($verbraucher);
return 'Der Energiefluss wurde erstellt oder aktualisiert.'; return 'Der Energiefluss wurde erstellt oder aktualisiert.';
} }
/** @return array<string, mixed> */ /** @return array<string, mixed> */
private function energieflussKnoten(string $ident, string $name, string $typ): array private function energieflussKnoten(
{ int $variableID,
string $name,
string $typ,
bool $gruppiert = false,
int $infoVariableID = 1
): array {
return [ return [
'VariableConfigType' => 0, 'VariableConfigType' => 0,
'VariableID' => $this->GetIDForIdent($ident), 'VariableID' => $variableID,
'SecondaryVariableID' => 1, 'SecondaryVariableID' => 1,
'Name' => $name, 'Name' => $name,
'Type' => $typ, 'Type' => $typ,
'Grouped' => false, 'Grouped' => $gruppiert,
'CalculationType' => 0, 'CalculationType' => 0,
'InfoVariableID' => 1, 'InfoVariableID' => $infoVariableID,
'Invert' => false, 'Invert' => false,
]; ];
} }
private function energieflussLeistungsprofil(): string
{
$konfiguration = $this->visualisierungsKonfiguration(
'EnergieflussKonfiguration'
);
if ($konfiguration['EnergieflussLeistungseinheit'] !== 1) {
return '~Power';
}
$profil = 'Enelix.Power.kW';
if (!IPS_VariableProfileExists($profil)) {
IPS_CreateVariableProfile($profil, 2);
IPS_SetVariableProfileValues($profil, -1000000.0, 1000000.0, 0.001);
IPS_SetVariableProfileDigits($profil, 3);
IPS_SetVariableProfileText($profil, '', ' kW');
}
return $profil;
}
private function energieflussLeistungsvariable(
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;
}
/** @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,
* Energie_Wh: float
* }>
*/
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)) {
continue;
}
$daten = $cache[(string) $instanzID]['Daten'] ?? null;
$istleistung = 0.0;
$sollleistung = 0.0;
if (is_array($daten)) {
$istleistung = $this->verbraucherleistung($daten);
$sollleistung = (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0);
}
$modulID = strtoupper(
(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' => $katalog === 'ev_charger' ? 'wallbox' : 'consumer',
'Istleistung_W' => $istleistung,
'Sollleistung_W' => $sollleistung,
'Energie_Wh' => is_numeric($verbraucherEnergie)
? (float) $verbraucherEnergie
: 0.0,
];
}
return $ergebnis;
}
/**
* @param list<array<string, mixed>>|null $verbraucher
*/
private function aktualisiereEnergieflusswerte(?array $verbraucher = null): void
{
if (!$this->ReadPropertyBoolean('EnergieflussAnzeigen')) {
return;
}
$verbraucher = $verbraucher ?? $this->energieflussVerbraucher();
$verbraucherleistung = 0.0;
foreach ($verbraucher as $eintrag) {
$this->setzeEnergieflussLeistungswert(
'EnergieflussIst_' . $eintrag['InstanzID'],
(float) $eintrag['Istleistung_W']
);
$this->setzeEnergieflussLeistungswert(
'EnergieflussSoll_' . $eintrag['InstanzID'],
(float) $eintrag['Sollleistung_W']
);
$verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']);
}
try {
$leistungen = $this->leseMessleistungen();
} catch (Throwable $fehler) {
$this->protokolliere('Energiefluss', $fehler->getMessage());
return;
}
$this->setzeEnergieflussLeistungswert('EnergieflussPV', $leistungen['PV']);
$this->setzeEnergieflussLeistungswert('EnergieflussNetz', $leistungen['Netz']);
$this->setzeEnergieflussLeistungswert(
'EnergieflussHaus',
max(0.0, $leistungen['Haus'] - $verbraucherleistung)
);
$this->setzeEnergieflussLeistungswert(
'EnergieflussBatterie',
$leistungen['Batterie'] + ($this->sdlAnzeigen('SDLEnergieflussAnzeigen') ? 0.0 : $leistungen['SDL'])
);
$this->setzeEnergieflussLeistungswert('EnergieflussSDL', $leistungen['SDL']);
$soc = $this->aktuelleSDLSOC();
if ($soc !== null) {
$this->SetValue('SDLSOCArchiv', $soc);
}
$flussID = $this->findeUnterobjekt('Energiefluss');
if ($flussID > 0) {
$this->synchronisiereSDLEnergiefluss($flussID);
}
}
private function setzeEnergieflussLeistungswert(string $ident, float $watt): void
{
$variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) {
return;
}
$konfiguration = $this->visualisierungsKonfiguration(
'EnergieflussKonfiguration'
);
$faktor = $konfiguration['EnergieflussLeistungseinheit'] === 1 ? 0.001 : 1.0;
SetValue($variableID, $watt * $faktor);
}
private function erstelleEnergiediagramme(): string private function erstelleEnergiediagramme(): string
{ {
$leistung = [ $konfiguration = $this->visualisierungsKonfiguration(
$this->chartDatensatz('PVLeistungArchiv', 'PV', '#F4C542', '~Power'), 'EnergiediagrammKonfiguration'
$this->chartDatensatz('HausverbrauchLeistungArchiv', 'Haus', '#22A06B', '~Power'), );
$this->chartDatensatz('NetzleistungArchiv', 'Netz', '#E95D5D', '~Power'), $pvAnlagen = $this->diagrammPVAnlagen();
$this->chartDatensatz('BatterieleistungArchiv', 'Batterie', '#8B5CF6', '~Power'), $batterien = $this->diagrammBatterien();
]; $verbraucher = $this->energieflussVerbraucher(
$energie = [ 'EnergiediagrammKonfiguration'
$this->chartDatensatz('PVEnergie', 'PV', '#F4C542', '~Electricity'), );
$this->chartDatensatz('Hausenergie', 'Haus', '#22A06B', '~Electricity'), $this->registriereDiagrammVariablen($pvAnlagen, $batterien, $verbraucher);
$this->chartDatensatz('NetzbezugEnergie', 'Netzbezug', '#E95D5D', '~Electricity'), $this->aktualisiereDiagrammMesswerte(true);
$this->chartDatensatz('EinspeisungEnergie', 'Einspeisung', '#2F80ED', '~Electricity'),
$this->chartDatensatz('BatterieLadenEnergie', 'Batterie laden', '#8B5CF6', '~Electricity'), $leistung = [];
$this->chartDatensatz('BatterieEntladenEnergie', 'Batterie entladen', '#F59E0B', '~Electricity'), $energie = [];
]; if ($konfiguration['DiagrammPVModus'] !== 1) {
$this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510); $leistung[] = $this->chartDatensatz(
$this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520); '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'
);
}
$leistungID = $this->erstelleDiagramm('Leistungsdiagramm', 'Leistungen', $leistung, 510);
$energieID = $this->erstelleDiagramm('Energiediagramm', 'Energien', $energie, 520);
$this->synchronisiereSDLDiagramm($leistungID, false);
$this->synchronisiereSDLDiagramm($energieID, true);
return 'Die Diagramme Leistungen und Energien wurden erstellt oder aktualisiert.'; 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 */ /** @param list<array<string, mixed>> $datensaetze */
private function erstelleDiagramm( private function erstelleDiagramm(
string $ident, string $ident,
@@ -320,7 +1098,8 @@ trait ManagerEnergieTrait
int $position int $position
): int { ): int {
$medienID = $this->findeUnterobjekt($ident); $medienID = $this->findeUnterobjekt($ident);
if ($medienID === 0) { $neu = $medienID === 0;
if ($neu) {
$medienID = IPS_CreateMedia(4); $medienID = IPS_CreateMedia(4);
IPS_SetParent($medienID, $this->InstanceID); IPS_SetParent($medienID, $this->InstanceID);
IPS_SetIdent($medienID, $ident); IPS_SetIdent($medienID, $ident);
@@ -329,6 +1108,11 @@ trait ManagerEnergieTrait
if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) { if ($objekt['ObjectType'] !== 5 || (int) IPS_GetMedia($medienID)['MediaType'] !== 4) {
throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.'); throw new RuntimeException('Unterobjekt ' . $ident . ' ist kein Diagramm.');
} }
if (!$neu) {
IPS_SetHidden($medienID, false);
return $medienID;
}
IPS_SetName($medienID, $name); IPS_SetName($medienID, $name);
IPS_SetPosition($medienID, $position); IPS_SetPosition($medienID, $position);
IPS_SetHidden($medienID, false); IPS_SetHidden($medienID, false);
@@ -345,16 +1129,17 @@ trait ManagerEnergieTrait
string $ident, string $ident,
string $titel, string $titel,
string $farbe, string $farbe,
string $profil string $profil,
string $typ = 'line'
): array { ): array {
return [ return [
'variableID' => $this->GetIDForIdent($ident), 'variableID' => $this->GetIDForIdent($ident),
'fillColor' => 'clear', 'fillColor' => $typ === 'bar' ? $farbe : 'clear',
'strokeColor' => $farbe, 'strokeColor' => $farbe,
'timeOffset' => 0, 'timeOffset' => 0,
'visible' => true, 'visible' => true,
'title' => $titel, 'title' => $titel,
'type' => 'line', 'type' => $typ,
'side' => 'left', 'side' => 'left',
'profile' => $profil, 'profile' => $profil,
]; ];
+302 -14
View File
@@ -19,6 +19,71 @@ final class ManagerRegler
public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV; public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV;
public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK; public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK;
/**
* @param list<array<string, mixed>> $punkte
*/
public static function interpolierteFahrplanVorgabe(
array $punkte,
int $jetzt,
int $maximalerPunktabstandSekunden = 1800
): ?float {
if ($maximalerPunktabstandSekunden < 0) {
throw new InvalidArgumentException('Maximaler Fahrplanpunktabstand darf nicht negativ sein.');
}
$gueltigePunkte = [];
foreach ($punkte as $punkt) {
if (!is_array($punkt)
|| !is_string($punkt['time'] ?? null)
|| (!is_int($punkt['gridTargetW'] ?? null)
&& !is_float($punkt['gridTargetW'] ?? null))
) {
continue;
}
$zeitpunkt = strtotime($punkt['time']);
$vorgabe = (float) $punkt['gridTargetW'];
if ($zeitpunkt === false || !is_finite($vorgabe)) {
continue;
}
$gueltigePunkte[$zeitpunkt] = $vorgabe;
}
if ($gueltigePunkte === []) {
return null;
}
ksort($gueltigePunkte, SORT_NUMERIC);
$vorherigerZeitpunkt = null;
$vorherigeVorgabe = null;
foreach ($gueltigePunkte as $zeitpunkt => $vorgabe) {
if ($zeitpunkt === $jetzt) {
return $vorgabe;
}
if ($zeitpunkt > $jetzt) {
if ($vorherigerZeitpunkt === null) {
return $zeitpunkt - $jetzt <= $maximalerPunktabstandSekunden
? $vorgabe
: null;
}
if ($jetzt - $vorherigerZeitpunkt > $maximalerPunktabstandSekunden
|| $zeitpunkt - $jetzt > $maximalerPunktabstandSekunden
) {
return null;
}
$anteil = ($jetzt - $vorherigerZeitpunkt) / ($zeitpunkt - $vorherigerZeitpunkt);
return $vorherigeVorgabe + (($vorgabe - $vorherigeVorgabe) * $anteil);
}
$vorherigerZeitpunkt = $zeitpunkt;
$vorherigeVorgabe = $vorgabe;
}
return $vorherigerZeitpunkt !== null
&& $jetzt - $vorherigerZeitpunkt <= $maximalerPunktabstandSekunden
? $vorherigeVorgabe
: null;
}
/** /**
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel * @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
*/ */
@@ -92,6 +157,7 @@ final class ManagerRegler
/** /**
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten * @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
* @param array<int, float|int> $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh
* @return array{ * @return array{
* Verteilbudget_W: int, * Verteilbudget_W: int,
* AktuelleVerbraucherleistung_W: int, * AktuelleVerbraucherleistung_W: int,
@@ -104,7 +170,8 @@ final class ManagerRegler
float $netzleistung, float $netzleistung,
float $zielnetzleistung, float $zielnetzleistung,
array $verbraucher, array $verbraucher,
string $prioritaetsfeld string $prioritaetsfeld,
array $bezogeneEnergieWh = []
): array { ): array {
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) { if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.'); throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
@@ -125,39 +192,102 @@ final class ManagerRegler
continue; continue;
} }
$festwert = self::istFestwert($daten['Leistungswerte_W']);
$festgehalten = !$daten['AenderungMoeglich'];
if ($festwert || $festgehalten) {
$nichtSteuerbar += $leistung;
}
$energie = $bezogeneEnergieWh[$eintrag['InstanzID']] ?? 0.0;
if ((!is_int($energie) && !is_float($energie))
|| (is_float($energie) && !is_finite($energie))
) {
throw new InvalidArgumentException('Bezogene Energie muss eine endliche Zahl sein.');
}
$steuerbar[] = [ $steuerbar[] = [
'InstanzID' => $eintrag['InstanzID'], 'InstanzID' => $eintrag['InstanzID'],
'Prioritaet' => $daten[$prioritaetsfeld], 'Prioritaet' => $daten[$prioritaetsfeld],
'Energiegruppe' => (int) round((float) $energie / 2000.0),
'Energie_Wh' => (float) $energie,
'Leistungswerte_W' => $daten['Leistungswerte_W'], 'Leistungswerte_W' => $daten['Leistungswerte_W'],
'Festwert' => $festwert,
'Festgehalten' => $festgehalten,
'Sollwert' => self::angenommenerSollwert($daten),
]; ];
} }
usort($steuerbar, static function (array $links, array $rechts): int { usort($steuerbar, static function (array $links, array $rechts): int {
return [$links['Prioritaet'], $links['InstanzID']] <=> [$rechts['Prioritaet'], $rechts['InstanzID']]; return [
$links['Prioritaet'],
$links['Energiegruppe'],
$links['InstanzID'],
] <=> [
$rechts['Prioritaet'],
$rechts['Energiegruppe'],
$rechts['InstanzID'],
];
}); });
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung); $verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
$positiveRichtung = $verteilbudget - $nichtSteuerbar >= 0;
$sollwerte = []; $sollwerte = [];
$minimumSumme = 0; $ausgangssumme = 0;
foreach ($steuerbar as $eintrag) { foreach ($steuerbar as $eintrag) {
$minimum = self::kleinsterLeistungswert($eintrag['Leistungswerte_W']); if ($eintrag['Festwert']) {
$sollwerte[$eintrag['InstanzID']] = $minimum; $sollwerte[$eintrag['InstanzID']] = self::kleinsterLeistungswert(
$minimumSumme += $minimum; $eintrag['Leistungswerte_W']
);
continue;
}
if ($eintrag['Festgehalten']) {
if ($eintrag['Sollwert'] !== null) {
$sollwerte[$eintrag['InstanzID']] = $eintrag['Sollwert'];
}
continue;
} }
$verfuegbar = max(0, $verteilbudget - $nichtSteuerbar - $minimumSumme); $ausgangswert = self::richtungsneutralerLeistungswert(
foreach ($steuerbar as $eintrag) {
$instanzID = $eintrag['InstanzID'];
$minimum = $sollwerte[$instanzID];
$neuerSollwert = self::groessterLeistungswertBis(
$eintrag['Leistungswerte_W'], $eintrag['Leistungswerte_W'],
$minimum + $verfuegbar $positiveRichtung
); );
$verfuegbar -= $neuerSollwert - $minimum; $sollwerte[$eintrag['InstanzID']] = $ausgangswert;
$ausgangssumme += $ausgangswert;
}
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
$gruppen = [];
foreach ($steuerbar as $eintrag) {
if ($eintrag['Festwert'] || $eintrag['Festgehalten']) {
continue;
}
$instanzID = $eintrag['InstanzID'];
$ausgangswert = $sollwerte[$instanzID];
if ($positiveRichtung) {
$gruppen[$eintrag['Prioritaet']][] = $eintrag;
continue;
} else {
if ($rest >= 0) {
continue;
}
$neuerSollwert = self::kleinsterLeistungswertAb(
$eintrag['Leistungswerte_W'],
$ausgangswert + $rest
);
}
$rest -= $neuerSollwert - $ausgangswert;
$sollwerte[$instanzID] = $neuerSollwert; $sollwerte[$instanzID] = $neuerSollwert;
} }
foreach ($gruppen as $gruppe) {
self::verteilePositiveGruppe($gruppe, $sollwerte, $rest);
}
$zugeteilt = $nichtSteuerbar + array_sum($sollwerte); $zugeteilt = $nichtSteuerbar;
foreach ($steuerbar as $eintrag) {
if (!$eintrag['Festwert'] && !$eintrag['Festgehalten']) {
$zugeteilt += $sollwerte[$eintrag['InstanzID']];
}
}
return [ return [
'Verteilbudget_W' => $verteilbudget, 'Verteilbudget_W' => $verteilbudget,
@@ -168,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 * @param array<string, mixed> $daten
*/ */
@@ -214,6 +433,53 @@ final class ManagerRegler
} }
} }
/**
* Ein einzelner Leistungswert bleibt als Befehl erhalten. Fuer die
* Leistungsbilanz zaehlt jedoch die gemessene Istleistung.
*
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/
private static function istFestwert(array $leistungswerte): bool
{
return count($leistungswerte) === 1 && is_int($leistungswerte[0]);
}
/**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/
private static function richtungsneutralerLeistungswert(
array $leistungswerte,
bool $positiveRichtung
): int {
$negativ = null;
$positiv = null;
foreach ($leistungswerte as $eintrag) {
if (is_int($eintrag)) {
if ($eintrag === 0) {
return 0;
}
$negativ = $eintrag < 0 ? max($negativ ?? $eintrag, $eintrag) : $negativ;
$positiv = $eintrag > 0 ? min($positiv ?? $eintrag, $eintrag) : $positiv;
continue;
}
if ($eintrag['Von_W'] <= 0 && $eintrag['Bis_W'] >= 0) {
return 0;
}
$negativ = $eintrag['Bis_W'] < 0
? max($negativ ?? $eintrag['Bis_W'], $eintrag['Bis_W'])
: $negativ;
$positiv = $eintrag['Von_W'] > 0
? min($positiv ?? $eintrag['Von_W'], $eintrag['Von_W'])
: $positiv;
}
if ($positiveRichtung) {
return $positiv ?? $negativ;
}
return $negativ ?? $positiv;
}
/** /**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte * @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/ */
@@ -247,4 +513,26 @@ final class ManagerRegler
return $gewaehlt; return $gewaehlt;
} }
/**
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
*/
private static function kleinsterLeistungswertAb(array $leistungswerte, int $untergrenze): int
{
foreach ($leistungswerte as $eintrag) {
if (is_int($eintrag)) {
if ($eintrag >= $untergrenze) {
return $eintrag;
}
continue;
}
if ($eintrag['Bis_W'] < $untergrenze) {
continue;
}
return max($eintrag['Von_W'], $untergrenze);
}
return self::richtungsneutralerLeistungswert($leistungswerte, false);
}
} }
+211
View File
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
trait ManagerSDLTrait
{
private function pruefeSDLKonfiguration(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return;
}
$leistungID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($leistungID)) {
throw new InvalidArgumentException('SDL benoetigt eine numerische Istleistungsvariable.');
}
$this->anlagenMessfaktor($this->ReadPropertyFloat('SDLLeistungsfaktor'), 'SDL');
$socID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($socID !== 0 && (!$this->istNumerischeVariable($socID) || $socID === $leistungID)) {
throw new InvalidArgumentException('SDL-SOC muss eine separate numerische Variable sein.');
}
$andereQuellen = array_merge($this->pvMessquellen(), $this->batterieMessquellen());
$andereQuellen[] = ['VariableID' => $this->ReadPropertyInteger('NetzleistungVariableID')];
foreach ($andereQuellen as $quelle) {
if ($leistungID === $quelle['VariableID']) {
throw new InvalidArgumentException(
'SDL-Leistung darf nicht zugleich als PV-, Netz- oder Batterieleistung bilanziert werden.'
);
}
}
}
private function synchronisiereSDLMesskonfiguration(): void
{
$neu = json_encode([
$this->ReadPropertyBoolean('SDLAktiv'),
$this->ReadPropertyInteger('SDLLeistungVariableID'),
$this->ReadPropertyFloat('SDLLeistungsfaktor'),
], JSON_THROW_ON_ERROR);
$alt = $this->ReadAttributeString('SDLMesskonfiguration');
if ($neu !== $alt && ($alt !== '' || $this->ReadPropertyBoolean('SDLAktiv'))) {
// Bereits erfasste Energien bleiben erhalten; keine Rueckrechnung mit neuen Quellen.
$this->WriteAttributeInteger('MessungLetzterZeitstempel', 0);
$this->WriteAttributeString('MessungZaehlerstaende', '{}');
$this->WriteAttributeInteger('MessungLetzteLeistungsaufzeichnung', 0);
}
$this->WriteAttributeString('SDLMesskonfiguration', $neu);
}
private function aktuelleSDLLeistung(): float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return 0.0;
}
$variableID = $this->ReadPropertyInteger('SDLLeistungVariableID');
if (!$this->istNumerischeVariable($variableID)) {
throw new RuntimeException('Die SDL-Istleistung ist nicht verfuegbar.');
}
$wert = GetValue($variableID);
if ((!is_int($wert) && !is_float($wert)) || !is_finite((float) $wert)) {
throw new RuntimeException('Die SDL-Istleistung ist keine endliche Zahl.');
}
$leistung = (float) $wert * $this->ReadPropertyFloat('SDLLeistungsfaktor');
if (!is_finite($leistung)) {
throw new RuntimeException('Die skalierte SDL-Istleistung ist nicht endlich.');
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') === 0 || $this->aktuelleSDLSOC() !== null) {
$this->setzeDiagnosewert('SDLStatus', 'Aktiv');
}
return $leistung;
}
private function aktuelleSDLSOC(): ?float
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
return null;
}
$variableID = $this->ReadPropertyInteger('SDLSOCVariableID');
if ($variableID === 0) {
return null;
}
if ($this->istNumerischeVariable($variableID)) {
$wert = (float) GetValue($variableID);
if (is_finite($wert) && $wert >= 0.0 && $wert <= 100.0) {
return $wert;
}
}
$this->setzeDiagnosewert('SDLStatus', 'Fehler: SDL-SOC muss zwischen 0 und 100 Prozent liegen.');
return null;
}
private function sdlAnzeigen(string $property): bool
{
return $this->ReadPropertyBoolean('SDLAktiv') && $this->ReadPropertyBoolean($property);
}
private function aktualisiereSDLStatus(): void
{
if (!$this->ReadPropertyBoolean('SDLAktiv')) {
$this->setzeDiagnosewert('SDLStatus', 'NichtVerwendet');
return;
}
try {
$this->aktuelleSDLLeistung();
$this->aktuelleSDLSOC();
} catch (Throwable $fehler) {
$this->setzeDiagnosewert('SDLStatus', 'Fehler: ' . $fehler->getMessage());
}
}
private function synchronisiereSDLEnergiefluss(int $instanzID, bool $profilAktualisieren = false): void
{
$anzeigen = $this->sdlAnzeigen('SDLEnergieflussAnzeigen');
$variableID = $this->findeUnterobjekt('EnergieflussSDL');
if (!$anzeigen && $variableID === 0) {
return;
}
if ($anzeigen && ($variableID === 0 || $profilAktualisieren)) {
$variableID = $this->energieflussLeistungsvariable(
'EnergieflussSDL', 'Energiefluss SDL', $this->energieflussLeistungsprofil(), 635
);
}
$alt = json_decode(IPS_GetProperty($instanzID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
if (!is_array($alt) || !array_is_list($alt)) {
throw new RuntimeException('Die bestehende Energiefluss-Konfiguration ist keine Liste.');
}
$neu = [];
$gefunden = false;
$socID = $this->aktuelleSDLSOC() !== null
? $this->GetIDForIdent('SDLSOCArchiv') : 1;
foreach ($alt as $knoten) {
if ((int) ($knoten['VariableID'] ?? 0) !== $variableID) {
$neu[] = $knoten;
} elseif ($anzeigen && !$gefunden) {
$knoten['InfoVariableID'] = $socID;
$neu[] = $knoten;
$gefunden = true;
}
}
if ($anzeigen && !$gefunden) {
$neu[] = $this->energieflussKnoten($variableID, 'SDL / Regelenergie', 'storage', false, $socID);
}
if ($neu !== $alt) {
IPS_SetProperty($instanzID, 'Config', json_encode($neu, JSON_THROW_ON_ERROR));
IPS_ApplyChanges($instanzID);
}
}
private function synchronisiereSDLDiagramm(int $medienID, bool $energie): void
{
$idents = $energie
? ['SDLLadenEnergie', 'SDLEntladenEnergie', 'SDLSOCArchiv']
: ['SDLLeistungArchiv', 'SDLSOCArchiv'];
$verwaltet = array_map(fn (string $ident): int => $this->GetIDForIdent($ident), $idents);
$gewuenscht = [];
if ($this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
if ($energie) {
$gewuenscht[] = $this->chartDatensatz(
'SDLLadenEnergie', 'SDL laden', '#007C91', '~Electricity', 'bar'
);
$gewuenscht[] = $this->chartDatensatz(
'SDLEntladenEnergie', 'SDL entladen', '#C43E1C', '~Electricity', 'bar'
);
} else {
$gewuenscht[] = $this->chartDatensatz(
'SDLLeistungArchiv', 'SDL Istleistung', '#007C91', '~Power'
);
}
if ($this->ReadPropertyInteger('SDLSOCVariableID') > 0) {
$soc = $this->chartDatensatz('SDLSOCArchiv', 'SDL SOC', '#C43E1C', '~Intensity.100');
$soc['side'] = 'right';
$gewuenscht[] = $soc;
}
}
$inhalt = json_decode((string) base64_decode(IPS_GetMediaContent($medienID), true), true);
if (!is_array($inhalt) || !isset($inhalt['datasets']) || !is_array($inhalt['datasets'])) {
if (!$this->sdlAnzeigen('SDLDiagrammeAnzeigen')) {
return;
}
throw new RuntimeException('Das bestehende Diagramm hat keine gueltigen Datensaetze.');
}
$alt = $inhalt['datasets'];
$neu = [];
foreach ($alt as $datensatz) {
$id = (int) ($datensatz['variableID'] ?? 0);
if (!in_array($id, $verwaltet, true)) {
$neu[] = $datensatz;
continue;
}
foreach ($gewuenscht as $index => $ziel) {
if ($id === $ziel['variableID']) {
$neu[] = $datensatz;
unset($gewuenscht[$index]);
break;
}
}
}
$neu = array_merge($neu, array_values($gewuenscht));
if ($neu !== $alt) {
$inhalt['datasets'] = $neu;
IPS_SetMediaContent($medienID, base64_encode(json_encode($inhalt, JSON_THROW_ON_ERROR)));
}
}
}
+19 -1
View File
@@ -5,13 +5,31 @@ declare(strict_types=1);
namespace Belevo\EnelixEMS; namespace Belevo\EnelixEMS;
/** /**
* Registriert die gemeinsamen Properties und Variablen aller EMS-Verbraucher. * Gemeinsame Registrierung und Sollwert-Bereinigung aller EMS-Verbraucher.
* *
* Das Trait ist noch keine vollstaendige Steuerungsimplementierung. Die * Das Trait ist noch keine vollstaendige Steuerungsimplementierung. Die
* konkreten Module ergaenzen Geraeteanbindung, Leistungsangebot und Zustand. * konkreten Module ergaenzen Geraeteanbindung, Leistungsangebot und Zustand.
*/ */
trait VerbraucherBasisTrait trait VerbraucherBasisTrait
{ {
protected function verwerfeSollwert(): void
{
// Lokale Ersatzleistungen nicht bei jedem weiteren Timeout kurz auf 0 setzen.
if (!(bool) $this->leseZustand('SollwertGueltig')) {
return;
}
$this->setzeZustand('SollwertGueltig', false);
$this->setzeZustand('Sollleistung', 0);
}
protected function bereinigeUngueltigenSollwert(): void
{
// Auch vor dem Update gespeicherte, bereits ungueltige Vorgaben bereinigen.
if (!(bool) $this->leseZustand('SollwertGueltig')) {
$this->setzeZustand('Sollleistung', 0);
}
}
protected function registriereVerbraucherBasis(): void protected function registriereVerbraucherBasis(): void
{ {
$this->RegisterPropertyInteger('PrioritaetPV', 0); $this->RegisterPropertyInteger('PrioritaetPV', 0);
+24
View File
@@ -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 public function testLoggingDiagnoseUndAktionsbuttonsSindVorhanden(): void
{ {
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
+32
View File
@@ -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 public function testWechselrichtersteuerungBietetNurNull(): void
{ {
self::assertSame( self::assertSame(
+27
View File
@@ -77,6 +77,33 @@ final class EaseeLadestatusTest extends TestCase
self::assertSame(0, $status['Phasenzahl']); self::assertSame(0, $status['Phasenzahl']);
} }
public function testStillstehendeWallboxSetztVeralteteLeistungUndStromAufNull(): void
{
$status = EaseeLadestatus::ausBeobachtungen([
'109' => 2,
'110' => 30,
'120' => 10.294,
'182' => 14.9,
'183' => 14.8,
'184' => 14.7,
], 32);
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
self::assertEqualsWithDelta(0.0, $status['Ladestrom_A'], 0.01);
}
public function testFertigGeladeneWallboxSetztVeralteteLeistungAufNull(): void
{
$status = EaseeLadestatus::ausBeobachtungen([
'109' => 4,
'110' => 30,
'120' => 10.294,
], 32);
self::assertTrue($status['FahrzeugGeladen']);
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
}
public function testApiUndKabelgrenzenBegrenzenDenKonfiguriertenStrom(): void public function testApiUndKabelgrenzenBegrenzenDenKonfiguriertenStrom(): void
{ {
$status = EaseeLadestatus::ausBeobachtungen([ $status = EaseeLadestatus::ausBeobachtungen([
+13
View File
@@ -70,6 +70,19 @@ final class EaseeModuleStrukturTest extends TestCase
self::assertStringContainsString("'Phasenzahl'", $modul); self::assertStringContainsString("'Phasenzahl'", $modul);
self::assertStringNotContainsString('7500', $modul); self::assertStringNotContainsString('7500', $modul);
self::assertStringNotContainsString('Abfrageintervall', $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); self::assertStringContainsString("'action' => 'StartCharging'", $modul);
$gateway = (string) file_get_contents( $gateway = (string) file_get_contents(
+80 -1
View File
@@ -24,6 +24,8 @@ final class EnergieMessungTest extends TestCase
self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001); self::assertEqualsWithDelta(0.15, $werte['EinspeisungEnergie'], 0.000001);
self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001); self::assertEqualsWithDelta(0.075, $werte['BatterieLadenEnergie'], 0.000001);
self::assertSame(0.0, $werte['BatterieEntladenEnergie']); self::assertSame(0.0, $werte['BatterieEntladenEnergie']);
self::assertSame(0.0, $werte['SDLLadenEnergie']);
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
} }
public function testTrenntBezugUndBatterieentladung(): void public function testTrenntBezugUndBatterieentladung(): void
@@ -50,23 +52,100 @@ final class EnergieMessungTest extends TestCase
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0)); self::assertSame(0.0, EnergieMessung::hausleistung(500.0, -1200.0, 0.0));
} }
public function testBeruecksichtigtSDLVorDerBegrenzungDerHausleistung(): void
{
self::assertSame(1700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 1000.0));
self::assertSame(3700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, -1000.0));
self::assertSame(1000.0, EnergieMessung::hausleistung(0.0, -1000.0, 0.0, -2000.0));
self::assertSame(0.0, EnergieMessung::hausleistung(500.0, 0.0, 0.0, 1000.0));
self::assertSame(
EnergieMessung::hausleistung(4200.0, -800.0, 700.0),
EnergieMessung::hausleistung(4200.0, -800.0, 700.0, 0.0)
);
}
public function testBerechnetHausenergieAusDenZaehlerdeltas(): void public function testBerechnetHausenergieAusDenZaehlerdeltas(): void
{ {
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0)); self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0));
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0)); self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 5.0, 0.0, 0.0));
} }
public function testBereinigtHausenergieUmSDLLadungUndEntladung(): void
{
self::assertSame(5.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 0.0));
self::assertSame(11.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 3.0));
self::assertSame(7.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 3.0, 2.0));
self::assertSame(1.0, EnergieMessung::hausenergie(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 2.0));
self::assertSame(0.0, EnergieMessung::hausenergie(1.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0));
self::assertSame(
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0),
EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, 0.0, 0.0)
);
}
public function testIntegriertSDLGetrenntVonBatterieUndHaus(): void
{
$leistungen = [
'PV' => 7200.0,
'Haus' => 3600.0,
'Netz' => -1800.0,
'Batterie' => 900.0,
'SDL' => 1800.0,
];
$laden = EnergieMessung::energieDeltas($leistungen, 300);
self::assertEqualsWithDelta(0.15, $laden['SDLLadenEnergie'], 0.000001);
self::assertSame(0.0, $laden['SDLEntladenEnergie']);
self::assertEqualsWithDelta(0.075, $laden['BatterieLadenEnergie'], 0.000001);
self::assertEqualsWithDelta(0.3, $laden['Hausenergie'], 0.000001);
$leistungen['SDL'] = -1800.0;
$entladen = EnergieMessung::energieDeltas($leistungen, 300);
self::assertSame(0.0, $entladen['SDLLadenEnergie']);
self::assertEqualsWithDelta(0.15, $entladen['SDLEntladenEnergie'], 0.000001);
self::assertSame($laden['Hausenergie'], $entladen['Hausenergie']);
self::assertSame($laden['BatterieLadenEnergie'], $entladen['BatterieLadenEnergie']);
$leistungen['SDL'] = 0.0;
$mitNull = EnergieMessung::energieDeltas($leistungen, 300);
unset($leistungen['SDL']);
self::assertSame($mitNull, EnergieMessung::energieDeltas($leistungen, 300));
}
public function testIgnoriertNichtEndlicheSDLWerte(): void
{
foreach ([INF, -INF, NAN] as $ungueltig) {
self::assertSame(2700.0, EnergieMessung::hausleistung(4200.0, -800.0, 700.0, $ungueltig));
self::assertSame(8.5, EnergieMessung::hausenergie(10.0, 2.0, 4.0, 1.5, 2.0, $ungueltig, $ungueltig));
$werte = EnergieMessung::energieDeltas([
'PV' => 0.0,
'Haus' => 1000.0,
'Netz' => 1000.0,
'Batterie' => 0.0,
'SDL' => $ungueltig,
], 3600);
self::assertSame(0.0, $werte['SDLLadenEnergie']);
self::assertSame(0.0, $werte['SDLEntladenEnergie']);
self::assertSame(1.0, $werte['Hausenergie']);
}
}
public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void public function testIgnoriertUngueltigeDauerUndNichtEndlicheWerte(): void
{ {
foreach ([-1, 0] as $dauer) {
foreach ([-1000.0, 1000.0] as $sdl) {
$werte = EnergieMessung::energieDeltas([ $werte = EnergieMessung::energieDeltas([
'PV' => INF, 'PV' => INF,
'Haus' => NAN, 'Haus' => NAN,
'Netz' => 1000.0, 'Netz' => 1000.0,
'Batterie' => -1000.0, 'Batterie' => -1000.0,
], -1); 'SDL' => $sdl,
], $dauer);
foreach ($werte as $wert) { foreach ($werte as $wert) {
self::assertSame(0.0, $wert); self::assertSame(0.0, $wert);
} }
} }
}
}
} }
+46 -7
View File
@@ -13,7 +13,7 @@ final class LadestationAdapterTest extends TestCase
/** /**
* @dataProvider statusFaelle * @dataProvider statusFaelle
* @param array<string, string> $konfiguration * @param array<string, string> $konfiguration
* @param array{Rohstatus: int, Istleistung_W: float} $erwartet * @param array{Rohstatus: int, Istleistung_W: float, Sollstrom_A?: int, Maximalstrom_A?: int, Iststrom_A?: float, Phasenzahl?: int} $erwartet
*/ */
public function testStatusaufrufUndDekodierungFuerAlleGeraete( public function testStatusaufrufUndDekodierungFuerAlleGeraete(
int $geraetetyp, int $geraetetyp,
@@ -47,16 +47,30 @@ final class LadestationAdapterTest extends TestCase
yield 'go-e alt' => [ yield 'go-e alt' => [
LadestationAdapter::GO_E_ALT, LadestationAdapter::GO_E_ALT,
$this->lokaleKonfiguration(), $this->lokaleKonfiguration(),
$this->goEAntwort(2, 1100), $this->goEAntwort(2, 1100, [160, 158, 159]),
'http://192.0.2.10/mqtt?payload=', 'http://192.0.2.10/mqtt?payload=',
['Rohstatus' => 2, 'Istleistung_W' => 11000.0], [
'Rohstatus' => 2,
'Istleistung_W' => 11000.0,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 16.0,
'Phasenzahl' => 3,
],
]; ];
yield 'go-e Gemini' => [ yield 'go-e Gemini' => [
LadestationAdapter::GO_E_GEMINI, LadestationAdapter::GO_E_GEMINI,
$this->lokaleKonfiguration(), $this->lokaleKonfiguration(),
$this->goEAntwort(2, 2300), $this->goEAntwort(2, 2300, [9.8, 0, 0]),
'http://192.0.2.10/api/status', 'http://192.0.2.10/api/status',
['Rohstatus' => 2, 'Istleistung_W' => 2300.0], [
'Rohstatus' => 2,
'Istleistung_W' => 2300.0,
'Sollstrom_A' => 16,
'Maximalstrom_A' => 16,
'Iststrom_A' => 9.8,
'Phasenzahl' => 1,
],
]; ];
yield 'Smart-me Pico' => [ yield 'Smart-me Pico' => [
LadestationAdapter::SMART_ME_PICO, LadestationAdapter::SMART_ME_PICO,
@@ -71,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 * @dataProvider steuerFaelle
* @param array<string, string> $konfiguration * @param array<string, string> $konfiguration
@@ -161,11 +189,22 @@ final class LadestationAdapterTest extends TestCase
]; ];
} }
private function goEAntwort(int $status, float $leistung): string /** @param list<int|float> $stroeme */
private function goEAntwort(int $status, float $leistung, array $stroeme): string
{ {
$nrg = array_fill(0, 12, 0); $nrg = array_fill(0, 12, 0);
foreach (array_values($stroeme) as $index => $strom) {
$nrg[4 + $index] = $strom;
}
$nrg[11] = $leistung; $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);
} }
} }
+85 -6
View File
@@ -9,6 +9,24 @@ use PHPUnit\Framework\TestCase;
final class LadestationReglerTest extends 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 public function testFahrzeugUndDreiPhasenWerdenWieInEnelixEinsErkannt(): void
{ {
$status = LadestationRegler::normalisiereStatus([ $status = LadestationRegler::normalisiereStatus([
@@ -22,23 +40,66 @@ final class LadestationReglerTest extends TestCase
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']); self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
} }
public function testEinphasigesFahrzeugWirdUnterhalb7500WErkannt(): void public function testDirektGemeldeteEinphasigkeitWirdAuchUnter7500WErkannt(): void
{ {
$status = LadestationRegler::normalisiereStatus([ $status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2, 'Rohstatus' => 2,
'Istleistung_W' => 2300.0, 'Istleistung_W' => 2300.0,
'Phasenzahl' => 1,
], false); ], false);
self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']); self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']);
self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01); self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01);
} }
public function testGeladenWirdNachVerbindungUnterSechsAmpereErkannt(): void public function testErkannteDreiphasigkeitBleibtWaehrendLadepauseErhalten(): void
{ {
$status = LadestationRegler::normalisiereStatus([ $status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2, 'Rohstatus' => 2,
'Istleistung_W' => 0.0, 'Istleistung_W' => 0.0,
], true); ], true, false, false, true, LadestationRegler::PHASEN_DREI);
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
self::assertSame(0.0, $status['Ladestrom_A']);
}
public function testPhasenBleibenOhneMessungBisZurErkennungUnbekannt(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], false);
self::assertSame(LadestationRegler::PHASEN_UNBEKANNT, $status['Phasenzahl']);
self::assertSame([0], LadestationRegler::leistungsangebot(
true,
true,
true,
false,
LadestationRegler::PHASEN_UNBEKANNT,
16,
true,
false
));
}
public function testAnlaufOhneGemesseneLadeleistungGiltNichtAlsLadeende(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, false, true, false);
self::assertFalse($status['FahrzeugGeladen']);
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testGeladenWirdErstNachZuvorGemessenerLadeleistungErkannt(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, false, true, true);
self::assertTrue($status['FahrzeugGeladen']); self::assertTrue($status['FahrzeugGeladen']);
self::assertSame(4, $status['Fahrzeugstatus']); self::assertSame(4, $status['Fahrzeugstatus']);
@@ -66,6 +127,24 @@ final class LadestationReglerTest extends TestCase
self::assertSame(4, $status['Fahrzeugstatus']); self::assertSame(4, $status['Fahrzeugstatus']);
} }
public function testDirekterPhasenstromHatVorrangVorNominalerLeistungsumrechnung(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 10263.0,
'Iststrom_A' => 15.9,
], true);
self::assertEqualsWithDelta(15.9, $status['Ladestrom_A'], 0.01);
}
public function testFahrzeugMaximalstromWirdMitZweiAmpereAbstandErkannt(): void
{
self::assertSame(18, LadestationRegler::ermittleFahrzeugMaximalstrom(20, 15.9, 20));
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(17, 15.9, 20));
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(20, 0.0, 20));
}
public function testBetriebsartUndSolarladenBestimmenDasLeistungsangebot(): void public function testBetriebsartUndSolarladenBestimmenDasLeistungsangebot(): void
{ {
$solarPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, false); $solarPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, false);
@@ -75,7 +154,7 @@ final class LadestationReglerTest extends TestCase
self::assertSame(0, $solarPV[0]); self::assertSame(0, $solarPV[0]);
self::assertCount(12, $solarPV); self::assertCount(12, $solarPV);
self::assertSame([3850], $normalPV); self::assertSame([3680], $normalPV);
self::assertSame([0], $solarPeak); self::assertSame([0], $solarPeak);
self::assertSame($solarPV, $normalPeak); self::assertSame($solarPV, $normalPeak);
} }
@@ -83,7 +162,7 @@ final class LadestationReglerTest extends TestCase
public function testPeakKannJedesFahrzeugAufNullSetzen(): void public function testPeakKannJedesFahrzeugAufNullSetzen(): void
{ {
self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3)); self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3));
self::assertSame(6, LadestationRegler::stromFuerLeistung(4150, 3)); self::assertSame(6, LadestationRegler::stromFuerLeistung(4104, 3));
self::assertSame(16, LadestationRegler::stromFuerLeistung(11000, 3)); self::assertSame(16, LadestationRegler::stromFuerLeistung(10944, 3));
} }
} }
@@ -68,10 +68,68 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
"RegisterPropertyBoolean('Ladefreigabe', true)", "RegisterPropertyBoolean('Ladefreigabe', true)",
$modul $modul
); );
self::assertStringContainsString(
"RegisterPropertyInteger('Phasenerkennungszeit', 60)",
$modul
);
self::assertStringContainsString(
"RegisterPropertyInteger('ZeitZwischenZustandswechseln', 1)",
$modul
);
self::assertStringContainsString(
"RegisterPropertyInteger('Mindesteinschaltdauer', 0)",
$modul
);
self::assertStringContainsString(
"RegisterPropertyInteger('Mindestausschaltdauer', 0)",
$modul
);
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul); self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
self::assertStringContainsString("EnableAction('Solarladen')", $modul); self::assertStringContainsString("EnableAction('Solarladen')", $modul);
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul); self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
self::assertStringContainsString("ReadPropertyBoolean('EinstellungenInVisu')", $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); 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
);
}
} }
+18 -4
View File
@@ -17,8 +17,14 @@ final class ManagerEnergieModulstrukturTest extends TestCase
foreach ([ foreach ([
"RegisterPropertyBoolean('EnergyPieAnzeigen', false)", "RegisterPropertyBoolean('EnergyPieAnzeigen', false)",
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)", "RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
"RegisterPropertyInteger('DiagrammPVModus', 0)",
"RegisterPropertyInteger('DiagrammBatterieModus', 0)",
"RegisterPropertyBoolean('LadestationenSeparatAnzeigen', true)",
"RegisterPropertyBoolean('VerbraucherSeparatAnzeigen', true)",
"RegisterAttributeBoolean('EnergieflussNeuErzeugen', false)",
"RegisterPropertyBoolean('FunFactsAnzeigen', false)", "RegisterPropertyBoolean('FunFactsAnzeigen', false)",
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)", "RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
"RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)",
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)", "RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)", "RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)", "RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
@@ -37,6 +43,10 @@ final class ManagerEnergieModulstrukturTest extends TestCase
'ENERGY_PIE_MODULE_ID', 'ENERGY_PIE_MODULE_ID',
'ENERGY_FLOW_MODULE_ID', 'ENERGY_FLOW_MODULE_ID',
'energieflussKnoten', 'energieflussKnoten',
"'type' => \$typ",
"'bar'",
'EnergiediagrammKonfiguration',
'EnergieflussKonfiguration',
'IPS_DeleteInstance', 'IPS_DeleteInstance',
'IPS_DeleteMedia', 'IPS_DeleteMedia',
] as $erwartet) { ] as $erwartet) {
@@ -44,10 +54,14 @@ final class ManagerEnergieModulstrukturTest extends TestCase
} }
foreach ([ foreach ([
'Energieaufzeichnung', 'Energieaufzeichnung',
'FormEnergyPieAnzeigen', 'EnergieflussLeistungseinheit',
'FormEnergiediagrammeAnzeigen', 'DiagrammPVModus',
'FormFunFactsAnzeigen', 'DiagrammBatterieModus',
'FormEnergieflussAnzeigen', 'LadestationenSeparatAnzeigen',
'VerbraucherSeparatAnzeigen',
'Bestehende Energy Pies, Diagramme und Energiefluesse bleiben bei Apply unveraendert',
'Wechselrichter / AC-Wandler',
'keine doppelte Batteriekonfiguration',
'MesswerteAnzeigen', 'MesswerteAnzeigen',
'IstleistungVariableID', 'IstleistungVariableID',
'LeistungVariableID', 'LeistungVariableID',
+44 -6
View File
@@ -51,6 +51,12 @@ final class ManagerModulstrukturTest extends TestCase
'EinspeisebegrenzungAktiv', 'EinspeisebegrenzungAktiv',
'Einspeisegrenze', 'Einspeisegrenze',
'PrognoseAktiv', 'PrognoseAktiv',
'SDLAktiv',
'SDLLeistungVariableID',
'SDLLeistungsfaktor',
'SDLSOCVariableID',
'SDLEnergieflussAnzeigen',
'SDLDiagrammeAnzeigen',
'PrognoseLizenzInformation', 'PrognoseLizenzInformation',
'NetzfahrplanAktiv', 'NetzfahrplanAktiv',
'NetzbezugEnergieVariableID', 'NetzbezugEnergieVariableID',
@@ -218,14 +224,28 @@ final class ManagerModulstrukturTest extends TestCase
self::assertStringContainsString("'issueDeviceToken' =>", $inhalt); self::assertStringContainsString("'issueDeviceToken' =>", $inhalt);
self::assertStringContainsString("unset(\$neueLease['deviceToken'])", $inhalt); self::assertStringContainsString("unset(\$neueLease['deviceToken'])", $inhalt);
self::assertStringContainsString( self::assertStringContainsString(
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))", "|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv')",
$inhalt $inhalt
); );
self::assertGreaterThanOrEqual(
3,
substr_count($inhalt, '$this->geraetezugangBenoetigt()'),
'Der Geraetezugang muss beim Anwenden, Erneuern und Anfordern beruecksichtigt werden.'
);
self::assertStringContainsString( self::assertStringContainsString(
"\$this->ReadPropertyString('Lizenzcode'),\n \$geraetezugangFehlt", "\$this->ReadPropertyString('Lizenzcode'),\n \$geraetezugangFehlt",
$inhalt $inhalt
); );
self::assertStringContainsString('synchronisiereAnlagentopologie()', $inhalt); self::assertStringContainsString('synchronisiereAnlagentopologie()', $inhalt);
self::assertStringContainsString('private function geraetezugangBenoetigt(): bool', $inhalt);
self::assertStringContainsString(
"if (\$this->aktualisiereLizenzfreigabe(\$wert, true)) {",
$inhalt
);
self::assertStringNotContainsString(
"if (!\$this->ReadPropertyBoolean('PrognoseAktiv')) {\n \$this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet');\n return;\n }\n\n try {",
$inhalt
);
self::assertStringContainsString( self::assertStringContainsString(
'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry', 'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry',
$inhalt $inhalt
@@ -308,11 +328,8 @@ final class ManagerModulstrukturTest extends TestCase
); );
self::assertGreaterThanOrEqual( self::assertGreaterThanOrEqual(
3, 3,
substr_count( substr_count($inhalt, '$this->geraetezugangBenoetigt()'),
$inhalt, 'Der Geraetezugang muss beim Anwenden, Erneuern und Anfordern beruecksichtigt werden.'
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))"
),
'Der Gerätezugang muss beim Anwenden, Erneuern und Anfordern berücksichtigt werden.'
); );
} }
@@ -369,4 +386,25 @@ final class ManagerModulstrukturTest extends TestCase
self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt); self::assertStringContainsString('VerbraucherdatenEmpfangen(array $daten)', $inhalt);
self::assertStringContainsString("'ManagerdatenEmpfangen'", $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']);
}
} }
+404 -3
View File
@@ -11,6 +11,59 @@ use PHPUnit\Framework\TestCase;
final class ManagerReglerTest extends TestCase final class ManagerReglerTest extends TestCase
{ {
public function testFahrplanVorgabeWirdZwischenPunktenLinearInterpoliert(): void
{
$punkte = [
['time' => '2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254],
['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283],
];
$vorgabe = ManagerRegler::interpolierteFahrplanVorgabe(
$punkte,
(int) strtotime('2026-09-30T11:32:30+00:00')
);
self::assertEqualsWithDelta(18075.2685, $vorgabe, 0.0001);
}
public function testFahrplanVorgabeVerwendetRandpunktInnerhalbDerToleranz(): void
{
$punkte = [
['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283],
['time' => '2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254],
];
self::assertSame(
15664.283,
ManagerRegler::interpolierteFahrplanVorgabe(
$punkte,
(int) strtotime('2026-09-30T11:29:44+00:00')
)
);
self::assertSame(
20486.254,
ManagerRegler::interpolierteFahrplanVorgabe(
$punkte,
(int) strtotime('2026-09-30T11:35:10+00:00')
)
);
}
public function testFahrplanVorgabeVerwirftZuGrosseZeitlicheLuecke(): void
{
$punkte = [
['time' => '2026-09-30T11:00:00+00:00', 'gridTargetW' => 10000],
['time' => '2026-09-30T12:00:00+00:00', 'gridTargetW' => 20000],
];
self::assertNull(
ManagerRegler::interpolierteFahrplanVorgabe(
$punkte,
(int) strtotime('2026-09-30T11:31:00+00:00')
)
);
}
public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void
{ {
self::assertSame( self::assertSame(
@@ -113,6 +166,206 @@ final class ManagerReglerTest extends TestCase
self::assertSame(500, $ergebnis['Restabweichung_W']); self::assertSame(500, $ergebnis['Restabweichung_W']);
} }
public function testGleicheNaechsteLeistungBevorzugtWenigerEnergie(): void
{
$ergebnis = ManagerRegler::verteile(
0.0,
3000.0,
[
$this->verbraucher(10, 0, [0, 3000], 0),
$this->verbraucher(20, 0, [0, 3000], 0),
],
'PrioritaetPV',
[10 => 3000.0, 20 => 0.0]
);
self::assertSame([20 => 3000, 10 => 0], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testEnergiegleichstandVergleichtExakteWhStattZweiKilowattstundenGruppen(): void
{
$ergebnis = ManagerRegler::verteile(
0.0,
3000.0,
[
$this->verbraucher(10, 0, [0, 3000], 0),
$this->verbraucher(20, 0, [0, 3000], 0),
],
'PrioritaetPV',
[10 => 999.0, 20 => 0.0]
);
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 public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
{ {
$fest = $this->verbraucher(10, 0, [3000], 3000, false); $fest = $this->verbraucher(10, 0, [3000], 3000, false);
@@ -120,12 +373,12 @@ final class ManagerReglerTest extends TestCase
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV'); $ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
self::assertSame(0, $ergebnis['NichtSteuerbar_W']); self::assertSame(3000, $ergebnis['NichtSteuerbar_W']);
self::assertSame([10 => 3000, 20 => 2000], $ergebnis['Sollwerte']); self::assertSame([10 => 3000, 20 => 2000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
} }
public function testFestwerteWerdenMitHoeherPriorisierterBatterieZugewiesen(): void public function testFestwerteWerdenNachIstleistungBilanziertUndAlsBefehlBeibehalten(): void
{ {
$ergebnis = ManagerRegler::verteile( $ergebnis = ManagerRegler::verteile(
-2411.0, -2411.0,
@@ -146,9 +399,10 @@ final class ManagerReglerTest extends TestCase
); );
self::assertSame(12562, $ergebnis['Verteilbudget_W']); self::assertSame(12562, $ergebnis['Verteilbudget_W']);
self::assertSame(10151, $ergebnis['NichtSteuerbar_W']);
self::assertSame( self::assertSame(
[ [
44234 => -12288, 44234 => 2411,
29743 => 0, 29743 => 0,
38676 => 0, 38676 => 0,
43878 => 13850, 43878 => 13850,
@@ -159,6 +413,132 @@ final class ManagerReglerTest extends TestCase
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
} }
public function testBatterieGleichtGemesseneFesteLastenAusOhneSolarladungZuSpeisen(): void
{
$ergebnis = ManagerRegler::verteile(
-4526.0,
-50.0,
[
$this->verbraucher(29743, 1, [0, 6500], 6500),
$this->verbraucher(38676, 1, [0, 4600], 4600),
$this->verbraucher(22078, 2, [7690], 0, false),
$this->verbraucher(31986, 2, [11000], 10942, false),
$this->verbraucher(43878, 2, [0, 4150, 10300], 9908),
$this->verbraucher(
44234,
6,
[['Von_W' => -39000, 'Bis_W' => 39000]],
-22769
),
],
'PrioritaetPV'
);
self::assertSame(13657, $ergebnis['Verteilbudget_W']);
self::assertSame(10942, $ergebnis['NichtSteuerbar_W']);
self::assertSame(
[
29743 => 0,
38676 => 0,
22078 => 7690,
31986 => 11000,
43878 => 0,
44234 => 2715,
],
$ergebnis['Sollwerte']
);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testBegrenztesFahrzeugLaedtVorBatterieOhneBatterieentladung(): void
{
$ergebnis = ManagerRegler::verteile(
-3587.0,
0.0,
[
$this->verbraucher(
10,
0,
[0, 4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700, 10300, 11000],
10263
),
$this->verbraucher(
20,
1,
[['Von_W' => -39000, 'Bis_W' => 39000]],
0
),
],
'PrioritaetPV'
);
self::assertSame(13850, $ergebnis['Verteilbudget_W']);
self::assertSame([10 => 11000, 20 => 2850], $ergebnis['Sollwerte']);
self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][20]);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testPvBetriebNutztBeiPositivemBudgetKeineNegativeLeistungsstufe(): void
{
$ergebnis = ManagerRegler::verteile(
0.0,
500.0,
[
$this->verbraucher(10, 0, [-1000, 5000], 0),
$this->verbraucher(20, 1, [0, 3000], 0),
],
'PrioritaetPV'
);
self::assertSame(500, $ergebnis['Verteilbudget_W']);
self::assertSame([10 => 5000, 20 => 0], $ergebnis['Sollwerte']);
self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][10]);
self::assertSame(-4500, $ergebnis['Restabweichung_W']);
}
public function testPvBetriebNutztNegativeLeistungErstBeiTatsaechlichemDefizit(): void
{
$ergebnis = ManagerRegler::verteile(
1000.0,
0.0,
[
$this->verbraucher(
10,
0,
[['Von_W' => -5000, 'Bis_W' => 5000]],
0
),
$this->verbraucher(20, 1, [0, 3000], 0),
],
'PrioritaetPV'
);
self::assertSame(-1000, $ergebnis['Verteilbudget_W']);
self::assertSame([10 => -1000, 20 => 0], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testFestgehaltenerVerbraucherBilanziertIstUndBehaeltSollwert(): void
{
$festgehalten = $this->verbraucher(10, 0, [0, 3000, 5000], 2000, false);
$festgehalten['Daten']['Zustand'][0]['Wert'] = 3000;
$ergebnis = ManagerRegler::verteile(
0.0,
3000.0,
[
$festgehalten,
$this->verbraucher(20, 1, [['Von_W' => 0, 'Bis_W' => 5000]], 0),
],
'PrioritaetPV'
);
self::assertSame(5000, $ergebnis['Verteilbudget_W']);
self::assertSame(2000, $ergebnis['NichtSteuerbar_W']);
self::assertSame([10 => 3000, 20 => 3000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
{ {
$verbraucher = $this->verbraucher(10, 0, [3000], null, false); $verbraucher = $this->verbraucher(10, 0, [3000], null, false);
@@ -173,6 +553,27 @@ final class ManagerReglerTest extends TestCase
self::assertSame(-3000, $ergebnis['Restabweichung_W']); self::assertSame(-3000, $ergebnis['Restabweichung_W']);
} }
public function testBatterieMitTieferPrioritaetUebernimmtNichtNutzbarenRest(): void
{
$ergebnis = ManagerRegler::verteile(
-5000.0,
0.0,
[
$this->verbraucher(20, 1, [0, 3000, 6000], 0),
$this->verbraucher(
10,
6,
[['Von_W' => 0, 'Bis_W' => 10000]],
0
),
],
'PrioritaetPV'
);
self::assertSame([20 => 3000, 10 => 2000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testMonatlicheGrenzeMussVollstaendigSein(): void public function testMonatlicheGrenzeMussVollstaendigSein(): void
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
+425
View File
@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
namespace Belevo\EnelixEMS\Tests;
use InvalidArgumentException;
use Manager;
use ManagerSDLRuntime;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use RuntimeException;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
final class ManagerSDLTest extends TestCase
{
private Manager $manager;
protected function setUp(): void
{
require_once __DIR__ . '/Support/ManagerSDLRuntime.php';
require_once __DIR__ . '/../Manager/module.php';
ManagerSDLRuntime::reset();
$this->manager = new Manager(42);
$this->manager->Create();
foreach ([101 => 1000.0, 102 => 5000.0, 103 => 200.0, 104 => 1500.0, 105 => 70.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
$this->properties([
'NetzleistungVariableID' => 101,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103],
]),
'SDLLeistungVariableID' => 104,
'SDLSOCVariableID' => 105,
]);
foreach ([
'SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie',
] as $ident) {
ManagerSDLRuntime::child(42, $ident);
}
ManagerSDLRuntime::child(42, 'SDLStatus', 3);
}
public function testSDLByDefaultInactiveDoesNotRequireSources(): void
{
self::assertFalse($this->manager->testProperties['SDLAktiv']);
self::assertFalse($this->manager->testProperties['SDLEnergieflussAnzeigen']);
self::assertFalse($this->manager->testProperties['SDLDiagrammeAnzeigen']);
unset(ManagerSDLRuntime::$variables[104], ManagerSDLRuntime::$variables[105]);
$this->invoke('pruefeSDLKonfiguration');
$powers = $this->invoke('leseMessleistungen');
self::assertSame(0.0, $powers['SDL']);
self::assertSame(5800.0, $powers['Haus']);
self::assertNull($this->invoke('aktuelleSDLSOC'));
}
/** @dataProvider signedPowerProvider */
public function testSDLPowerIsExcludedFromHouseConsumption(float $raw, float $factor, float $sdl, float $house): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
ManagerSDLRuntime::variable(104, $raw);
$this->invoke('pruefeSDLKonfiguration');
self::assertSame([
'PV' => 5000.0, 'Haus' => $house, 'Netz' => 1000.0, 'Batterie' => 200.0, 'SDL' => $sdl,
], $this->invoke('leseMessleistungen'));
}
public static function signedPowerProvider(): array
{
return [
'charging' => [1500.0, 1.0, 1500.0, 4300.0],
'discharging' => [-1500.0, 1.0, -1500.0, 7300.0],
'kilowatts' => [2.5, 1000.0, 2500.0, 3300.0],
'inverted source' => [1500.0, -1.0, -1500.0, 7300.0],
];
}
/** @dataProvider duplicatePowerProvider */
public function testRejectsSDLPowerAlsoUsedByAnotherEnergySource(int $duplicateID): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungVariableID' => $duplicateID]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('nicht zugleich');
$this->invoke('pruefeSDLKonfiguration');
}
public static function duplicatePowerProvider(): array
{
return ['grid' => [101], 'PV' => [102], 'battery' => [103]];
}
public function testRejectsSOCUsingThePowerVariable(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 104]);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('separate numerische Variable');
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider invalidFactorProvider */
public function testRejectsInvalidSDLFactors(float $factor): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => $factor]);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
public static function invalidFactorProvider(): array
{
return ['zero' => [0.0], 'infinite' => [INF], 'not a number' => [NAN]];
}
public function testMissingSDLPowerFailsWithoutReturningHouseConsumption(): void
{
$this->properties(['SDLAktiv' => true]);
unset(ManagerSDLRuntime::$variables[104]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('nicht verfuegbar');
$this->invoke('leseMessleistungen');
}
/** @dataProvider invalidPowerProvider */
public function testNonfiniteOrNonnumericPowerDoesNotBecomeZero($value): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, $value);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('keine endliche Zahl');
$this->invoke('leseMessleistungen');
}
public static function invalidPowerProvider(): array
{
return ['infinite' => [INF], 'negative infinite' => [-INF], 'not a number' => [NAN], 'string' => ['1500']];
}
public function testScaledSDLPowerOverflowIsRejected(): void
{
$this->properties(['SDLAktiv' => true, 'SDLLeistungsfaktor' => 2.0]);
ManagerSDLRuntime::variable(104, PHP_FLOAT_MAX);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('skalierte SDL-Istleistung');
$this->invoke('leseMessleistungen');
}
public function testSDLPowerMustBeANumericVariable(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(104, '1500', 3);
$this->expectException(InvalidArgumentException::class);
$this->invoke('pruefeSDLKonfiguration');
}
/** @dataProvider socProvider */
public function testSDLSOCBoundaries(float $value, ?float $expected): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, $value);
self::assertSame($expected, $this->invoke('aktuelleSDLSOC'));
if ($expected === null) {
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
}
public static function socProvider(): array
{
return [
'empty' => [0.0, 0.0], 'full' => [100.0, 100.0], 'normal' => [37.5, 37.5],
'negative' => [-0.1, null], 'overfull' => [100.1, null], 'infinite' => [INF, null], 'NaN' => [NAN, null],
];
}
public function testSDLSOCMayBeOmittedAndDisappearingSourceIsIgnored(): void
{
$this->properties(['SDLAktiv' => true, 'SDLSOCVariableID' => 0]);
$this->invoke('pruefeSDLKonfiguration');
self::assertNull($this->invoke('aktuelleSDLSOC'));
$this->properties(['SDLSOCVariableID' => 105]);
unset(ManagerSDLRuntime::$variables[105]);
self::assertNull($this->invoke('aktuelleSDLSOC'));
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
}
public function testPowerReadingPreservesSOCErrorUntilRecovery(): void
{
$this->properties(['SDLAktiv' => true]);
ManagerSDLRuntime::variable(105, 101.0);
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
$this->invoke('aktuelleSDLLeistung');
self::assertStringContainsString('SDL-SOC', $this->manager->GetValue('SDLStatus'));
ManagerSDLRuntime::variable(105, 47.5);
$this->invoke('aktuelleSDLLeistung');
self::assertSame('Aktiv', $this->manager->GetValue('SDLStatus'));
}
public function testSDLSOCDoesNotChangeWeightedNormalBatterySOC(): void
{
ManagerSDLRuntime::variable(106, 20.0);
ManagerSDLRuntime::variable(107, 80.0);
$this->properties([
'SDLAktiv' => true,
'AnlagenBatterien' => $this->json([
['ID' => 'small', 'SOCVariableID' => 106, 'Nutzkapazitaet_kWh' => 10.0],
['ID' => 'large', 'SOCVariableID' => 107, 'Nutzkapazitaet_kWh' => 30.0],
]),
]);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
self::assertSame(70.0, $this->invoke('aktuelleSDLSOC'));
ManagerSDLRuntime::variable(105, 0.0);
self::assertSame(65.0, $this->invoke('aktuelleBatterieSoc'));
}
public function testCounterDeltasExcludeSDLChargingAndIncludeDischarging(): void
{
$this->configureCounters();
$baseline = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertCount(8, $baseline);
self::assertSame(array_fill_keys(array_keys($baseline), 0.0), $baseline);
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertEqualsWithDelta(2.1, $deltas['Hausenergie'], 0.000001);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
self::assertSame(0.2, $deltas['SDLEntladenEnergie']);
self::assertEqualsWithDelta(0.4, $deltas['BatterieLadenEnergie'], 0.000001);
self::assertEqualsWithDelta(0.1, $deltas['BatterieEntladenEnergie'], 0.000001);
}
public function testMissingSDLIntervalDoesNotPolluteHouseEnergyOrTheNextInterval(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.0, 0.0, false]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertEqualsWithDelta(2.0, $deltas['PVEnergie'], 0.000001);
self::assertEqualsWithDelta(1.0, $deltas['NetzbezugEnergie'], 0.000001);
ManagerSDLRuntime::variable(201, 102.5);
$next = $this->invoke('leseEnergieZaehlerDeltas', [0.1, 0.0, true]);
self::assertEqualsWithDelta(0.4, $next['Hausenergie'], 0.000001);
}
public function testCounterResetSuppressesHouseEnergyForActiveSDL(): void
{
$this->configureCounters();
$this->invoke('leseEnergieZaehlerDeltas');
$this->advanceCounters();
ManagerSDLRuntime::variable(202, 0.0);
$deltas = $this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]);
self::assertSame(0.0, $deltas['Hausenergie']);
self::assertSame(0.0, $deltas['NetzbezugEnergie']);
self::assertSame(0.6, $deltas['SDLLadenEnergie']);
}
public function testWithoutConfiguredCountersReturnsNull(): void
{
$this->properties(['SDLAktiv' => true]);
self::assertNull($this->invoke('leseEnergieZaehlerDeltas', [0.6, 0.2]));
}
public function testChangingSDLSourceResetsBaselinesButKeepsCollectedEnergy(): void
{
$this->properties(['SDLAktiv' => true]);
$this->invoke('synchronisiereSDLMesskonfiguration');
$this->manager->testAttributes['MessungLetzterZeitstempel'] = 123;
$this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung'] = 100;
$this->manager->testAttributes['MessungZaehlerstaende'] = '{"pv:pv":5.0}';
$this->manager->testAttributes['MessungEnergiePuffer'] = '{"Hausenergie":1.5}';
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(123, $this->manager->testAttributes['MessungLetzterZeitstempel']);
$this->properties(['SDLLeistungsfaktor' => -1.0]);
$this->invoke('synchronisiereSDLMesskonfiguration');
self::assertSame(0, $this->manager->testAttributes['MessungLetzterZeitstempel']);
self::assertSame(0, $this->manager->testAttributes['MessungLetzteLeistungsaufzeichnung']);
self::assertSame('{}', $this->manager->testAttributes['MessungZaehlerstaende']);
self::assertSame('{"Hausenergie":1.5}', $this->manager->testAttributes['MessungEnergiePuffer']);
}
/** @dataProvider chartProvider */
public function testChartMergePreservesCustomizationsAndOnlyRemovesSDL(bool $energy): void
{
$this->properties(['SDLAktiv' => true, 'SDLDiagrammeAnzeigen' => true]);
$powerIdent = $energy ? 'SDLLadenEnergie' : 'SDLLeistungArchiv';
$custom = ['variableID' => 999, 'title' => 'Custom series', 'strokeColor' => '#ABCDEF'];
$sdl = ['variableID' => $this->manager->GetIDForIdent($powerIdent), 'title' => 'Own SDL name', 'visible' => false];
ManagerSDLRuntime::$media[301] = base64_encode($this->json([
'datasets' => [$custom, $sdl], 'customOption' => ['keep' => true],
]));
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
$chart = $this->chart();
self::assertSame($custom, $chart['datasets'][0]);
self::assertSame($sdl, $chart['datasets'][1]);
self::assertSame(['keep' => true], $chart['customOption']);
self::assertCount($energy ? 4 : 3, $chart['datasets']);
$soc = $chart['datasets'][count($chart['datasets']) - 1];
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $soc['variableID']);
self::assertSame('right', $soc['side']);
if ($energy) {
self::assertSame('bar', $chart['datasets'][2]['type']);
self::assertSame($this->manager->GetIDForIdent('SDLEntladenEnergie'), $chart['datasets'][2]['variableID']);
}
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLDiagrammeAnzeigen' => false]);
$this->invoke('synchronisiereSDLDiagramm', [301, $energy]);
self::assertSame([$custom], $this->chart()['datasets']);
self::assertSame(['keep' => true], $this->chart()['customOption']);
}
public static function chartProvider(): array
{
return ['power chart' => [false], 'energy chart' => [true]];
}
public function testEnergyFlowMergePreservesCustomNodesAndIsIdempotent(): void
{
$this->properties(['SDLAktiv' => true, 'SDLEnergieflussAnzeigen' => true]);
$custom = ['VariableID' => 999, 'Name' => 'Custom consumer', 'Type' => 'consumer', 'Invert' => true];
ManagerSDLRuntime::$properties[302]['Config'] = $this->json([$custom]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
$nodes = $this->flow();
self::assertSame($custom, $nodes[0]);
self::assertCount(2, $nodes);
self::assertSame('storage', $nodes[1]['Type']);
self::assertSame($this->manager->GetIDForIdent('EnergieflussSDL'), $nodes[1]['VariableID']);
self::assertSame($this->manager->GetIDForIdent('SDLSOCArchiv'), $nodes[1]['InfoVariableID']);
self::assertTrue(ManagerSDLRuntime::$hidden[$nodes[1]['VariableID']]);
$nodes[1]['Name'] = 'My SDL';
$nodes[1]['Invert'] = true;
ManagerSDLRuntime::$properties[302]['Config'] = $this->json($nodes);
$writes = ManagerSDLRuntime::$writes;
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame($nodes, $this->flow());
self::assertSame($writes, ManagerSDLRuntime::$writes);
$this->properties(['SDLSOCVariableID' => 0]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame(1, $this->flow()[1]['InfoVariableID']);
self::assertSame('My SDL', $this->flow()[1]['Name']);
$this->properties(['SDLAktiv' => false]);
$this->invoke('synchronisiereSDLEnergiefluss', [302]);
self::assertSame([$custom], $this->flow());
}
private function configureCounters(): void
{
$this->properties([
'SDLAktiv' => true,
'NetzbezugEnergieVariableID' => 202,
'NetzeinspeisungEnergieVariableID' => 203,
'AnlagenWechselrichter' => $this->json([
['ID' => 'pv', 'Typ' => 'pv', 'IstleistungVariableID' => 102, 'ErzeugungsenergieVariableID' => 201],
]),
'AnlagenBatterien' => $this->json([
['ID' => 'battery', 'LeistungVariableID' => 103, 'LadeenergieVariableID' => 204, 'EntladeenergieVariableID' => 205],
]),
]);
foreach ([201 => 100.0, 202 => 10.0, 203 => 5.0, 204 => 10.0, 205 => 8.0] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
private function advanceCounters(): void
{
foreach ([201 => 102.0, 202 => 11.0, 203 => 5.2, 204 => 10.4, 205 => 8.1] as $id => $value) {
ManagerSDLRuntime::variable($id, $value);
}
}
private function properties(array $properties): void
{
$this->manager->testProperties = array_replace($this->manager->testProperties, $properties);
}
private function invoke(string $name, array $arguments = [])
{
$method = new ReflectionMethod(Manager::class, $name);
if (PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}
return $method->invokeArgs($this->manager, $arguments);
}
private function json(array $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
}
private function chart(): array
{
return json_decode(base64_decode(ManagerSDLRuntime::$media[301]), true, 512, JSON_THROW_ON_ERROR);
}
private function flow(): array
{
return json_decode(ManagerSDLRuntime::$properties[302]['Config'], true, 512, JSON_THROW_ON_ERROR);
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
/** Minimal Symcon runtime, loaded only inside isolated ManagerSDLTest processes. */
final class ManagerSDLRuntime
{
public static array $variables = [];
public static array $children = [];
public static array $properties = [];
public static array $media = [];
public static array $writes = [];
public static array $hidden = [];
private static int $nextID = 10000;
public static function reset(): void
{
self::$variables = [];
self::$children = [];
self::$properties = [];
self::$media = [];
self::$writes = [];
self::$hidden = [];
self::$nextID = 10000;
}
public static function variable(int $id, $value, int $type = 2): int
{
self::$variables[$id] = ['VariableType' => $type, 'Value' => $value];
return $id;
}
public static function child(int $parent, string $ident, int $type = 2): int
{
if (!isset(self::$children[$parent][$ident])) {
$id = self::$nextID++;
self::$children[$parent][$ident] = $id;
self::variable($id, $type === 3 ? '' : 0.0, $type);
}
return self::$children[$parent][$ident];
}
}
class IPSModule
{
public int $InstanceID;
public array $testProperties = [];
public array $testAttributes = [];
public function __construct(int $instanceID)
{
$this->InstanceID = $instanceID;
}
public function Create(): void
{
}
public function __call(string $method, array $arguments)
{
if (preg_match('/^(Register|Read|Write)(Property|Attribute)(Boolean|Integer|Float|String)$/', $method, $parts)) {
$storage = $parts[2] === 'Property' ? 'testProperties' : 'testAttributes';
$ident = $arguments[0];
if ($parts[1] === 'Register') {
$this->{$storage}[$ident] = $this->{$storage}[$ident] ?? $arguments[1];
return null;
}
if ($parts[1] === 'Write') {
$this->{$storage}[$ident] = $arguments[1];
return null;
}
if (!array_key_exists($ident, $this->{$storage})) {
throw new RuntimeException('Unregistered test property or attribute: ' . $ident);
}
return $this->{$storage}[$ident];
}
if (preg_match('/^RegisterVariable(Boolean|Integer|Float|String)$/', $method, $parts)) {
$types = ['Boolean' => 0, 'Integer' => 1, 'Float' => 2, 'String' => 3];
return ManagerSDLRuntime::child($this->InstanceID, $arguments[0], $types[$parts[1]]);
}
throw new BadMethodCallException('Unsupported Symcon test call: ' . $method);
}
public function EnableAction(string $ident): void
{
}
public function RegisterTimer(string $ident, int $interval, string $script): void
{
}
public function GetIDForIdent(string $ident): int
{
if (!isset(ManagerSDLRuntime::$children[$this->InstanceID][$ident])) {
throw new RuntimeException('Unknown test child: ' . $ident);
}
return ManagerSDLRuntime::$children[$this->InstanceID][$ident];
}
public function SetValue(string $ident, $value): void
{
ManagerSDLRuntime::$variables[$this->GetIDForIdent($ident)]['Value'] = $value;
}
public function GetValue(string $ident)
{
return GetValue($this->GetIDForIdent($ident));
}
}
function IPS_VariableExists(int $variableID): bool
{
return isset(ManagerSDLRuntime::$variables[$variableID]);
}
function IPS_GetVariable(int $variableID): array
{
if (!IPS_VariableExists($variableID)) {
throw new RuntimeException('Unknown test variable: ' . $variableID);
}
return ManagerSDLRuntime::$variables[$variableID];
}
function GetValue(int $variableID)
{
return IPS_GetVariable($variableID)['Value'];
}
function IPS_GetObjectIDByIdent(string $ident, int $parentID)
{
return ManagerSDLRuntime::$children[$parentID][$ident] ?? false;
}
function IPS_GetProperty(int $instanceID, string $property)
{
return ManagerSDLRuntime::$properties[$instanceID][$property];
}
function IPS_SetProperty(int $instanceID, string $property, $value): void
{
ManagerSDLRuntime::$properties[$instanceID][$property] = $value;
ManagerSDLRuntime::$writes[] = ['property', $instanceID, $property];
}
function IPS_ApplyChanges(int $instanceID): void
{
ManagerSDLRuntime::$writes[] = ['apply', $instanceID];
}
function IPS_GetMediaContent(int $mediaID): string
{
return ManagerSDLRuntime::$media[$mediaID];
}
function IPS_SetMediaContent(int $mediaID, string $content): void
{
ManagerSDLRuntime::$media[$mediaID] = $content;
ManagerSDLRuntime::$writes[] = ['media', $mediaID];
}
function IPS_SetHidden(int $objectID, bool $hidden): void
{
ManagerSDLRuntime::$hidden[$objectID] = $hidden;
}
+32
View File
@@ -133,6 +133,7 @@ PHP
'GoodWe-Entladen verwendet negativen Managerwert und Modus 12', 'GoodWe-Entladen verwendet negativen Managerwert und Modus 12',
static function (TestContext $test) use ( static function (TestContext $test) use (
$managerdatenSenden, $managerdatenSenden,
$batterieID,
$modusID, $modusID,
$leistungID $leistungID
): void { ): void {
@@ -141,6 +142,10 @@ PHP
static fn (): bool => (float) GetValue($leistungID) === 843.0 static fn (): bool => (float) GetValue($leistungID) === 843.0
&& (int) GetValue($modusID) === 12 && (int) GetValue($modusID) === 12
); );
$test->assertSame(-843, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
IPS_RequestAction($batterieID, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $batterieID)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
} }
); );
@@ -163,6 +168,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( $test->runCase(
'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus', 'Wechselrichtersteuerung setzt Leistung auf null und Automatikmodus',
static function (TestContext $test) use ( static function (TestContext $test) use (
@@ -171,6 +202,7 @@ PHP
$leistungID $leistungID
): void { ): void {
$test->configureInstance($batterieID, ['Batteriemanagement' => 1]); $test->configureInstance($batterieID, ['Batteriemanagement' => 1]);
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $batterieID)));
$test->waitUntil( $test->waitUntil(
static fn (): bool => (float) GetValue($leistungID) === 0.0 static fn (): bool => (float) GetValue($leistungID) === 0.0
&& (int) GetValue($managementID) === 1 && (int) GetValue($managementID) === 1
+32
View File
@@ -24,6 +24,38 @@ return static function (TestContext $test): void {
); );
}); });
$test->runCase('SignalR-Abbruch synchronisiert den Gatewaystatus', static function (
TestContext $test
) use ($gateway): void {
IPS_RequestAction(
$gateway,
'TestSignalRFrame',
json_encode(['type' => 7], JSON_THROW_ON_ERROR)
);
$test->assertInstanceStatus($gateway, 203);
$test->assertSame(
false,
GetValue($test->objectByIdent('Connected', $gateway))
);
$test->assertSame(
'Easee-Ereignisverbindung wurde beendet.',
GetValue($test->objectByIdent('LastError', $gateway))
);
IPS_RequestAction($gateway, 'TestSignalRFrame', '{}');
$test->assertInstanceStatus($gateway);
$test->assertSame(
true,
GetValue($test->objectByIdent('Connected', $gateway))
);
$test->assertSame(
'',
GetValue($test->objectByIdent('LastError', $gateway))
);
});
$test->runCase('Abonnement liefert injizierten API-Zustand', static function ( $test->runCase('Abonnement liefert injizierten API-Zustand', static function (
TestContext $test TestContext $test
) use ($gateway): void { ) use ($gateway): void {
+120 -2
View File
@@ -95,7 +95,7 @@ return static function (TestContext $test): void {
) use ($station): void { ) use ($station): void {
IPS_RequestAction($station, 'Aktiv', true); IPS_RequestAction($station, 'Aktiv', true);
$test->assertSame( $test->assertSame(
'[0,4150,4850,5550,6250,6950,7600,8300,9000,9700,10300,11000]', '[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944]',
(string) GetValue( (string) GetValue(
$test->objectByIdent('LeistungsangebotDiagnose', $station) $test->objectByIdent('LeistungsangebotDiagnose', $station)
) )
@@ -125,7 +125,7 @@ return static function (TestContext $test): void {
); );
$test->assertInstanceStatus($station); $test->assertInstanceStatus($station);
$test->assertSame( $test->assertSame(
'[3850]', '[3680]',
(string) GetValue( (string) GetValue(
$test->objectByIdent('LeistungsangebotDiagnose', $station) $test->objectByIdent('LeistungsangebotDiagnose', $station)
) )
@@ -184,6 +184,104 @@ return static function (TestContext $test): void {
$test->assertInstanceStatus($station); $test->assertInstanceStatus($station);
}); });
$test->runCase('Solarumschaltung wartet auf die neue Manager-Vorgabe', static function (
TestContext $test
) use ($station, $sendeBeobachtung): 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(
0,
GetValue($test->objectByIdent('Sollleistung', $station)),
'Die verworfene Manager-Vorgabe darf nicht als aktuelle Sollleistung stehen bleiben.'
);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$sendeSollwert(10944);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)),
'"Ampere":16'
));
IPS_RequestAction($station, 'Solarladen', true);
$sendeSollwert(0);
$test->assertSame(true, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
$sendeSollwert(10944);
$sendeBeobachtung('EH123456', 109, 1);
$test->assertSame('[0]', GetValue($test->objectByIdent('LeistungsangebotDiagnose', $station)));
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)),
'"Ampere":0'
));
$sendeBeobachtung('EH123456', 110, 30);
$sendeBeobachtung('EH123456', 109, 3);
$sendeSollwert(10944);
IPS_RequestAction($station, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $station)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $station)));
});
$test->runCase('SignalR-Abbruch erreicht die Ladestation', static function (
TestContext $test
) use ($gateway, $station): void {
IPS_RequestAction(
$gateway,
'TestSignalRFrame',
json_encode(['type' => 7], JSON_THROW_ON_ERROR)
);
$test->assertInstanceStatus($gateway, 203);
$test->assertInstanceStatus($station, 202);
$test->assertSame(
false,
GetValue($test->objectByIdent('GatewayVerbunden', $station))
);
IPS_RequestAction($gateway, 'TestSignalRFrame', '{}');
$test->assertInstanceStatus($gateway);
$test->assertInstanceStatus($station);
$test->assertSame(
true,
GetValue($test->objectByIdent('GatewayVerbunden', $station))
);
});
$test->runCase('Gateway-Ausfall verwirft Fahrzeug- und Phasenstatus', static function ( $test->runCase('Gateway-Ausfall verwirft Fahrzeug- und Phasenstatus', static function (
TestContext $test TestContext $test
) use ($gateway, $station): void { ) use ($gateway, $station): void {
@@ -200,4 +298,24 @@ return static function (TestContext $test): void {
); );
$test->assertInstanceStatus($station, 202); $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);
});
}; };
+353 -35
View File
@@ -6,8 +6,11 @@ use Belevo\EnelixEMS\SymconTest\TestContext;
return static function (TestContext $test): void { return static function (TestContext $test): void {
$testPasswort = hash('sha256', __FILE__); $testPasswort = hash('sha256', __FILE__);
$goEAntwort = static function (int $status, float $leistung): string { $goEAntwort = static function (int $status, float $leistung, array $stroeme = []): string {
$nrg = array_fill(0, 12, 0); $nrg = array_fill(0, 12, 0);
foreach (array_values($stroeme) as $index => $strom) {
$nrg[4 + $index] = $strom;
}
$nrg[11] = $leistung; $nrg[11] = $leistung;
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR); return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
}; };
@@ -16,14 +19,14 @@ return static function (TestContext $test): void {
'go-e alt' => [ 'go-e alt' => [
'Geraetetyp' => 1, 'Geraetetyp' => 1,
'Geraeteadresse' => '192.0.2.10', 'Geraeteadresse' => '192.0.2.10',
'Testantwort' => $goEAntwort(2, 1100), 'Testantwort' => $goEAntwort(2, 1100, [160, 158, 159]),
'Phasenzahl' => 3, 'Phasenzahl' => 3,
'BefehlUrlTeil' => 'amp=16', 'BefehlUrlTeil' => 'amp=16',
], ],
'go-e Gemini' => [ 'go-e Gemini' => [
'Geraetetyp' => 2, 'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.11', 'Geraeteadresse' => '192.0.2.11',
'Testantwort' => $goEAntwort(2, 2300), 'Testantwort' => $goEAntwort(2, 2300, [10, 0, 0]),
'Phasenzahl' => 1, 'Phasenzahl' => 1,
'BefehlUrlTeil' => 'amp=16', 'BefehlUrlTeil' => 'amp=16',
], ],
@@ -47,6 +50,7 @@ return static function (TestContext $test): void {
foreach ($definitionen as $name => $definition) { foreach ($definitionen as $name => $definition) {
$properties = array_merge($definition, [ $properties = array_merge($definition, [
'MaximalerLadestrom' => 16, 'MaximalerLadestrom' => 16,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true, 'Ladefreigabe' => true,
'Solarladen' => true, 'Solarladen' => true,
'EinstellungenInVisu' => true, 'EinstellungenInVisu' => true,
@@ -68,6 +72,7 @@ return static function (TestContext $test): void {
'Geraetetyp' => 1, 'Geraetetyp' => 1,
'Geraeteadresse' => '192.0.2.12', 'Geraeteadresse' => '192.0.2.12',
'MaximalerLadestrom' => 16, 'MaximalerLadestrom' => 16,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true, 'Ladefreigabe' => true,
'Solarladen' => true, 'Solarladen' => true,
'EinstellungenInVisu' => true, 'EinstellungenInVisu' => true,
@@ -77,6 +82,96 @@ return static function (TestContext $test): void {
] ]
); );
$stromerkennung = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Fahrzeugstromerkennung',
[
'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.13',
'MaximalerLadestrom' => 20,
'FahrzeugstromErkennungszeit' => 1,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 10260, [15.9, 15.8, 15.7]),
]
);
$phasenerkennung = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Phasenerkennung',
[
'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.14',
'MaximalerLadestrom' => 16,
'Phasenerkennungszeit' => 1,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 0),
]
);
$phasenhaltung = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Phasenhaltung',
[
'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.14',
'MaximalerLadestrom' => 16,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
]
);
$schaltsperre = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Schaltsperre',
[
'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.15',
'MaximalerLadestrom' => 16,
'ZeitZwischenZustandswechseln' => 1,
'Mindesteinschaltdauer' => 2,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
]
);
$mindestzeiten = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Mindestzeiten',
[
'Geraetetyp' => 2,
'Geraeteadresse' => '192.0.2.17',
'MaximalerLadestrom' => 16,
'ZeitZwischenZustandswechseln' => 0,
'Mindesteinschaltdauer' => 1,
'Mindestausschaltdauer' => 1,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
]
);
$managerId = $test->createInstance( $managerId = $test->createInstance(
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}', '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
'Hilfsmanager Ladestation', 'Hilfsmanager Ladestation',
@@ -84,7 +179,16 @@ return static function (TestContext $test): void {
'AutomatischeSuche' => false, 'AutomatischeSuche' => false,
'VerbraucherZuordnung' => json_encode(array_map( 'VerbraucherZuordnung' => json_encode(array_map(
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true], static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
array_values($stationen) array_merge(
array_values($stationen),
[
$stromerkennung,
$phasenerkennung,
$phasenhaltung,
$schaltsperre,
$mindestzeiten,
]
)
), JSON_THROW_ON_ERROR), ), JSON_THROW_ON_ERROR),
] ]
); );
@@ -105,10 +209,25 @@ return static function (TestContext $test): void {
'Sollleistung_W' => $leistung, 'Sollleistung_W' => $leistung,
], JSON_THROW_ON_ERROR)); ], 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 ( $test->runCase('Alle drei Geraetevarianten erkennen Fahrzeug und Phasen', static function (
TestContext $test TestContext $test
) use ($definitionen, $stationen): void { ) use ($definitionen, $stationen, $warteAufGeraetebefehl): void {
foreach ($stationen as $name => $stationId) { foreach ($stationen as $name => $stationId) {
IPS_RequestAction($stationId, 'Aktiv', true); IPS_RequestAction($stationId, 'Aktiv', true);
$test->assertSame(true, GetValue($test->objectByIdent('FahrzeugVerbunden', $stationId))); $test->assertSame(true, GetValue($test->objectByIdent('FahrzeugVerbunden', $stationId)));
@@ -118,7 +237,10 @@ return static function (TestContext $test): void {
); );
$test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))); $test->assertSame(false, GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)));
IPS_RequestAction($stationId, 'Solarladen', false); IPS_RequestAction($stationId, 'Solarladen', false);
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)); $befehl = $warteAufGeraetebefehl(
$stationId,
$definitionen[$name]['BefehlUrlTeil']
);
$diagnose = json_encode([ $diagnose = json_encode([
'Aktiv' => GetValue($test->objectByIdent('Aktiv', $stationId)), 'Aktiv' => GetValue($test->objectByIdent('Aktiv', $stationId)),
'Ladefreigabe' => GetValue($test->objectByIdent('Ladefreigabe', $stationId)), 'Ladefreigabe' => GetValue($test->objectByIdent('Ladefreigabe', $stationId)),
@@ -136,6 +258,94 @@ return static function (TestContext $test): void {
} }
}); });
$test->runCase('Erkannte Phasen bleiben waehrend einer Ladepause stabil', static function (
TestContext $test
) use ($goEAntwort, $phasenhaltung): void {
IPS_RequestAction($phasenhaltung, 'Aktiv', true);
$test->assertSame(
3,
GetValue($test->objectByIdent('Phasenzahl', $phasenhaltung))
);
IPS_SetProperty($phasenhaltung, 'Testantwort', $goEAntwort(2, 0));
IPS_ApplyChanges($phasenhaltung);
IPS_RequestAction($phasenhaltung, 'Statuszyklus', false);
$test->assertSame(
3,
GetValue($test->objectByIdent('Phasenzahl', $phasenhaltung))
);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $phasenhaltung))
);
});
$test->runCase('Schaltsperre haelt die aktuelle Leistung stabil', static function (
TestContext $test
) use ($schaltsperre, $anforderungSenden): void {
IPS_RequestAction($schaltsperre, 'Aktiv', true);
IPS_RequestAction($schaltsperre, 'Solarladen', true);
$anforderungSenden($schaltsperre, 4104);
IPS_RequestAction($schaltsperre, 'Statuszyklus', false);
$diagnose = json_encode([
'Angebot' => GetValue($test->objectByIdent('LeistungsangebotDiagnose', $schaltsperre)),
'Verbunden' => GetValue($test->objectByIdent('FahrzeugVerbunden', $schaltsperre)),
'Geladen' => GetValue($test->objectByIdent('FahrzeugGeladen', $schaltsperre)),
'Phasen' => GetValue($test->objectByIdent('Phasenzahl', $schaltsperre)),
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $schaltsperre)),
'SollwertGueltig' => GetValue($test->objectByIdent('SollwertGueltig', $schaltsperre)),
'Befehl' => GetValue($test->objectByIdent('LetzterGeraetebefehl', $schaltsperre)),
], JSON_THROW_ON_ERROR);
$test->assertSame(
'[4104]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $schaltsperre)),
'Diagnose: ' . $diagnose
);
$abgewiesen = false;
try {
$anforderungSenden($schaltsperre, 4788);
} catch (Throwable $fehler) {
$abgewiesen = true;
}
if (!$abgewiesen) {
$abgewiesen = GetValue(
$test->objectByIdent('SollwertGueltig', $schaltsperre)
) !== true || GetValue(
$test->objectByIdent('Sollleistung', $schaltsperre)
) !== 4788;
}
$test->assertSame(true, $abgewiesen);
});
$test->runCase('Mindestzeiten erlauben Modulation aber verhindern Takten', static function (
TestContext $test
) use ($mindestzeiten, $anforderungSenden): void {
IPS_RequestAction($mindestzeiten, 'Aktiv', true);
$anforderungSenden($mindestzeiten, 4104);
IPS_RequestAction($mindestzeiten, 'Statuszyklus', false);
$angebot = json_decode(
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $mindestzeiten)),
true,
512,
JSON_THROW_ON_ERROR
);
$test->assertTrue(!in_array(0, $angebot, true));
$test->assertTrue(in_array(4788, $angebot, true));
$anforderungSenden($mindestzeiten, 4788);
IPS_RequestAction($mindestzeiten, 'Ladefreigabe', false);
IPS_RequestAction($mindestzeiten, 'Ladefreigabe', true);
$test->assertSame(
'[0]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $mindestzeiten))
);
});
$test->runCase('Solarpause wird nicht als Ladeende erkannt', static function ( $test->runCase('Solarpause wird nicht als Ladeende erkannt', static function (
TestContext $test TestContext $test
) use ($solarPause): void { ) use ($solarPause): void {
@@ -151,35 +361,12 @@ return static function (TestContext $test): void {
); );
}); });
$test->runCase('ApplyChanges prueft ein gespeichertes Ladeende neu', static function (
TestContext $test
) use ($goEAntwort, $solarPause): void {
IPS_RequestAction($solarPause, 'Solarladen', false);
IPS_RequestAction($solarPause, 'Statuszyklus', false);
$test->assertSame(
true,
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
);
IPS_SetProperty($solarPause, 'Testantwort', $goEAntwort(2, 2300));
IPS_ApplyChanges($solarPause);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
);
$test->assertSame(
3,
GetValue($test->objectByIdent('Fahrzeugstatus', $solarPause))
);
});
$test->runCase('Solar- und Peakbetrieb liefern die Enelix-1-Angebote', static function ( $test->runCase('Solar- und Peakbetrieb liefern die Enelix-1-Angebote', static function (
TestContext $test TestContext $test
) use ($stationen, $anforderungSenden): void { ) use ($stationen, $anforderungSenden): void {
$stationId = $stationen['go-e alt']; $stationId = $stationen['go-e alt'];
IPS_RequestAction($stationId, 'Solarladen', true); IPS_RequestAction($stationId, 'Solarladen', true);
$anforderungSenden($stationId, 4150); $anforderungSenden($stationId, 4104);
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)); $befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
$diagnose = json_encode([ $diagnose = json_encode([
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)), 'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)),
@@ -192,24 +379,36 @@ return static function (TestContext $test): void {
'amp=6' 'amp=6'
), 'Solarladen: Der 6-A-Befehl fehlt. Erhalten: ' . $befehl . '; Diagnose: ' . $diagnose); ), 'Solarladen: Der 6-A-Befehl fehlt. Erhalten: ' . $befehl . '; Diagnose: ' . $diagnose);
$befehlVorUmschaltung = $befehl;
IPS_RequestAction($stationId, 'Solarladen', false); 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(
0,
GetValue($test->objectByIdent('Sollleistung', $stationId)),
'Die verworfene Manager-Vorgabe darf nicht als aktuelle Sollleistung stehen bleiben.'
);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)); $angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
$test->assertSame('[11000]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.'); $test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
$abgewiesen = false; $abgewiesen = false;
try { try {
$anforderungSenden($stationId, 4150); $anforderungSenden($stationId, 4104);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
$abgewiesen = true; $abgewiesen = true;
} }
if (!$abgewiesen) { if (!$abgewiesen) {
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $stationId)) !== true $abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $stationId)) !== true
|| GetValue($test->objectByIdent('Sollleistung', $stationId)) !== 4150; || GetValue($test->objectByIdent('Sollleistung', $stationId)) !== 4104;
} }
$test->assertSame(true, $abgewiesen, 'Normalbetrieb muss eine Zwischenstufe ablehnen.'); $test->assertSame(true, $abgewiesen, 'Normalbetrieb muss eine Zwischenstufe ablehnen.');
$anforderungSenden($stationId, 4150, 'Peak'); $anforderungSenden($stationId, 4104, 'Peak');
$test->assertSame( $test->assertSame(
'[0,4150,4850,5550,6250,6950,7600,8300,9000,9700,10300,11000]', '[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)) (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
); );
$test->assertTrue(str_contains( $test->assertTrue(str_contains(
@@ -228,4 +427,123 @@ return static function (TestContext $test): void {
'alw=0' 'alw=0'
), 'Peak mit Solarladen muss die Ladestation stoppen.'); ), 'Peak mit Solarladen muss die Ladestation stoppen.');
}); });
$test->runCase('Fahrzeugstrom wird nach stabiler Unterschreitung begrenzt', static function (
TestContext $test
) use ($stromerkennung, $anforderungSenden): void {
IPS_RequestAction($stromerkennung, 'Aktiv', true);
IPS_RequestAction($stromerkennung, 'Solarladen', true);
$anforderungSenden($stromerkennung, 13680);
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
IPS_Sleep(2100);
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
$test->assertSame(
18,
GetValue($test->objectByIdent('FahrzeugMaximalstrom', $stromerkennung))
);
$test->assertSame(
'[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944,11628,12312]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stromerkennung))
);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stromerkennung)),
'amp=18'
));
});
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Mindeststrom erkannt', static function (
TestContext $test
) use ($phasenerkennung, $anforderungSenden): void {
$stationId = $phasenerkennung;
$test->assertSame(
0,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertTrue(!str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=6'
), 'Eine inaktive Station darf keine Phasenprobe starten.');
IPS_RequestAction($stationId, 'Aktiv', true);
$test->assertSame(
0,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=6'
), 'Nach der Freigabe muss die Mindeststrom-Phasenprobe starten.');
IPS_Sleep(2100);
IPS_RequestAction($stationId, 'Statuszyklus', false);
$anforderungSenden($stationId, 3680);
IPS_RequestAction($stationId, 'Statuszyklus', false);
$test->assertSame(
1,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))
);
$test->assertSame(
true,
GetValue($test->objectByIdent('SollwertGueltig', $stationId))
);
$test->assertSame(
3680,
GetValue($test->objectByIdent('Sollleistung', $stationId))
);
$test->assertSame(
'[0,1380,1610,1840,2070,2300,2530,2760,2990,3220,3450,3680]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
);
});
$test->runCase('Abziehen und Vorgabe-Timeout hinterlassen keine alte Sollleistung', static function (
TestContext $test
) use ($stationen, $goEAntwort, $anforderungSenden): void {
$stationId = $stationen['go-e alt'];
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 10944, [160, 160, 160]));
IPS_ApplyChanges($stationId);
IPS_RequestAction($stationId, 'Aktiv', true);
IPS_RequestAction($stationId, 'Solarladen', true);
$anforderungSenden($stationId, 10944);
$test->assertSame(10944, GetValue($test->objectByIdent('Sollleistung', $stationId)));
IPS_RequestAction($stationId, 'VorgabeTimeout', 0);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $stationId)));
$anforderungSenden($stationId, 10944);
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(1, 0));
IPS_ApplyChanges($stationId);
$test->assertSame('[0]', GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)));
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $stationId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $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);
});
}; };
+235 -22
View File
@@ -6,8 +6,13 @@ use Belevo\EnelixEMS\SymconTest\TestContext;
return static function (TestContext $test): void { return static function (TestContext $test): void {
$netzleistungId = $test->createVariable(2, 'Netzleistung', -800.0); $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); $pvLeistungId = $test->createVariable(2, 'PV-Leistung', 4200.0);
$pvEnergieId = $test->createVariable(2, 'PV-Energie', 120.0);
$batterieLeistungId = $test->createVariable(2, 'Batterieleistung', 700.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); $batterieSocId = $test->createVariable(2, 'Batterie-Ladezustand', 62.0);
$einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig'); $einKontaktId = $test->createActionVariable('Kontakt Verbraucher 1-Stufig');
$einStufigId = $test->createInstance( $einStufigId = $test->createInstance(
@@ -82,6 +87,8 @@ return static function (TestContext $test): void {
'ACNennleistung_kW' => 10.0, 'ACNennleistung_kW' => 10.0,
'IstleistungVariableID' => $pvLeistungId, 'IstleistungVariableID' => $pvLeistungId,
'Istleistungsfaktor' => 1.0, 'Istleistungsfaktor' => 1.0,
'ErzeugungsenergieVariableID' => $pvEnergieId,
'Erzeugungsenergiefaktor' => 1.0,
]]; ]];
$pvKonfiguration = [[ $pvKonfiguration = [[
'ID' => 'pv-1', 'ID' => 'pv-1',
@@ -106,6 +113,9 @@ return static function (TestContext $test): void {
'LeistungVariableID' => $batterieLeistungId, 'LeistungVariableID' => $batterieLeistungId,
'Leistungsfaktor' => 1.0, 'Leistungsfaktor' => 1.0,
'SOCVariableID' => $batterieSocId, 'SOCVariableID' => $batterieSocId,
'LadeenergieVariableID' => $batterieLadeenergieId,
'EntladeenergieVariableID' => $batterieEntladeenergieId,
'Energiefaktor' => 1.0,
]]; ]];
$lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE')); $lizenzcode = trim((string) getenv('ENELIX_TEST_LICENSE_CODE'));
$managerId = $test->createInstance( $managerId = $test->createInstance(
@@ -113,6 +123,8 @@ return static function (TestContext $test): void {
'Manager', 'Manager',
[ [
'NetzleistungVariableID' => $netzleistungId, 'NetzleistungVariableID' => $netzleistungId,
'NetzbezugEnergieVariableID' => $netzbezugEnergieId,
'NetzeinspeisungEnergieVariableID' => $netzeinspeisungEnergieId,
'EnergyPieAnzeigen' => true, 'EnergyPieAnzeigen' => true,
'EnergiediagrammeAnzeigen' => true, 'EnergiediagrammeAnzeigen' => true,
'FunFactsAnzeigen' => true, 'FunFactsAnzeigen' => true,
@@ -226,11 +238,17 @@ return static function (TestContext $test): void {
}); });
$test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function ( $test->runCase('Energiewerte, Archivregeln und Unterobjekte werden erzeugt', static function (
TestContext $test TestContext $test
) use ($managerId): void { ) use ($managerId, $pvLeistungId): void {
IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true); IPS_RequestAction($managerId, 'MesswerteAufzeichnen', true);
$test->assertSame( $test->assertSame(
4200.0, 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( $test->assertSame(
2700.0, 2700.0,
@@ -268,11 +286,89 @@ return static function (TestContext $test): void {
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}', '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID'] IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID']
); );
$knoten = json_decode(
(string) IPS_GetProperty($energieflussID, 'Config'),
true,
512,
JSON_THROW_ON_ERROR
);
$namen = array_column($knoten, 'Name');
foreach (['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'] as $name) {
$test->assertTrue(in_array($name, $namen, true));
}
foreach ($knoten as $knotenEintrag) {
if (in_array(
$knotenEintrag['Name'],
['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'],
true
)) {
$test->assertTrue((int) $knotenEintrag['InfoVariableID'] > 1);
}
}
$pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId);
$test->assertSame(
4200.0,
(float) GetValue($pvFlussID),
'Der anfaengliche PV-Energiefluss ist falsch.'
);
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false); $pieID = $test->objectByIdent('EnergyPie', $managerId);
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); $leistungsdiagrammID = $test->objectByIdent('Leistungsdiagramm', $managerId);
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', true); $energiediagrammID = $test->objectByIdent('Energiediagramm', $managerId);
$test->assertSame(false, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']); $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));
}
$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 ([ foreach ([
'EnergyPieAnzeigen', 'EnergyPieAnzeigen',
@@ -295,7 +391,135 @@ return static function (TestContext $test): void {
$ident . ' wurde beim Ausschalten nicht entfernt.' $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));
}
});
$test->runCase('SDL korrigiert Hauslast und ergaenzt bestehende Anzeigen idempotent', static function (
TestContext $test
) use ($managerId): void {
$leistungID = $test->createVariable(2, 'SDL Istleistung', -1.2);
$socID = $test->createVariable(2, 'SDL SOC', 47.5);
$flussID = $test->objectByIdent('Energiefluss', $managerId);
$chartID = $test->objectByIdent('Leistungsdiagramm', $managerId);
$flussVorher = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
$chartVorher = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
try {
IPS_SetProperty($managerId, 'SDLAktiv', true);
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $leistungID);
IPS_SetProperty($managerId, 'SDLLeistungsfaktor', -1000.0);
IPS_SetProperty($managerId, 'SDLSOCVariableID', $socID);
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', true);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
IPS_ApplyChanges($managerId);
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
$test->assertSame(1200.0, GetValue($test->objectByIdent('SDLLeistungArchiv', $managerId)));
$test->assertSame(47.5, GetValue($test->objectByIdent('SDLSOCArchiv', $managerId)));
$test->assertSame(1.2, GetValue($test->objectByIdent('EnergieflussSDL', $managerId)));
$test->assertSame('Aktiv', GetValue($test->objectByIdent('SDLStatus', $managerId)));
$fluss = json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame($flussVorher, array_slice($fluss, 0, count($flussVorher)));
$test->assertSame(count($flussVorher) + 1, count($fluss));
$test->assertSame($test->objectByIdent('SDLSOCArchiv', $managerId), $fluss[count($fluss) - 1]['InfoVariableID']);
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame($chartVorher['datasets'], array_slice($chart['datasets'], 0, count($chartVorher['datasets'])));
$test->assertSame('right', $chart['datasets'][count($chart['datasets']) - 1]['side']);
$test->assertSame('~Intensity.100', $chart['datasets'][count($chart['datasets']) - 1]['profile']);
IPS_ApplyChanges($managerId);
$test->assertSame($fluss, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame($chart, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
foreach ([0 => ['~Power', 1200.0], 1 => ['Enelix.Power.kW', 1.2]] as $einheit => $erwartet) {
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', false);
IPS_ApplyChanges($managerId);
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', $einheit);
IPS_SetProperty($managerId, 'EnergieflussAnzeigen', true);
IPS_ApplyChanges($managerId);
$flussID = $test->objectByIdent('Energiefluss', $managerId);
$sdlID = $test->objectByIdent('EnergieflussSDL', $managerId);
$test->assertSame($erwartet[0], IPS_GetVariable($sdlID)['VariableProfile']);
$test->assertSame($erwartet[1], GetValue($sdlID));
}
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
IPS_ApplyChanges($managerId);
$test->assertSame($flussVorher, json_decode(IPS_GetProperty($flussID, 'Config'), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame($chartVorher, json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR));
$test->assertSame(1500.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
$test->assertEquals(1.9, GetValue($test->objectByIdent('EnergieflussBatterie', $managerId)), 0.000001);
IPS_SetProperty($managerId, 'SDLSOCVariableID', 0);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', true);
IPS_ApplyChanges($managerId);
$chart = json_decode(base64_decode(IPS_GetMediaContent($chartID)), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame(count($chartVorher['datasets']) + 1, count($chart['datasets']));
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', false);
$test->assertTrue(IPS_ApplyChanges($managerId));
$archivID = IPS_GetInstanceListByModuleID('{43192F0B-135B-4CE7-A0A7-1475603F3060}')[0];
foreach (['SDLLeistungArchiv', 'SDLSOCArchiv', 'SDLLadenEnergie', 'SDLEntladenEnergie'] as $ident) {
$test->assertSame(false, AC_GetLoggingStatus($archivID, $test->objectByIdent($ident, $managerId)));
}
} finally {
IPS_SetProperty($managerId, 'EnergieaufzeichnungAktiv', true);
IPS_SetProperty($managerId, 'SDLAktiv', false);
IPS_SetProperty($managerId, 'SDLEnergieflussAnzeigen', false);
IPS_SetProperty($managerId, 'SDLDiagrammeAnzeigen', false);
IPS_ApplyChanges($managerId);
}
$test->assertSame(2700.0, GetValue($test->objectByIdent('HausverbrauchLeistungArchiv', $managerId)));
});
$test->runCase('SDL weist fehlende und doppelte Leistungsquellen zurueck', static function (
TestContext $test
) use ($managerId, $batterieLeistungId): void {
try {
IPS_SetProperty($managerId, 'SDLAktiv', true);
foreach ([0, $batterieLeistungId] as $quelle) {
IPS_SetProperty($managerId, 'SDLLeistungVariableID', $quelle);
IPS_ApplyChanges($managerId);
$test->assertInstanceStatus($managerId, 202);
}
} finally {
IPS_SetProperty($managerId, 'SDLAktiv', false);
IPS_SetProperty($managerId, 'SDLLeistungVariableID', 0);
IPS_ApplyChanges($managerId);
}
}); });
$zustandswert = static function (array $daten, int $instanzId, string $kennung) { $zustandswert = static function (array $daten, int $instanzId, string $kennung) {
@@ -416,12 +640,10 @@ return static function (TestContext $test): void {
SetValue($temperaturId, 53.0); SetValue($temperaturId, 53.0);
$test->waitUntil(static function () use ( $test->waitUntil(static function () use (
$managerId,
$warmwasserId, $warmwasserId,
$verbraucherstatus, $verbraucherstatus,
$zustandswert $zustandswert
): bool { ): bool {
IPS_RequestAction($managerId, 'Regeln', false);
$wert = $zustandswert( $wert = $zustandswert(
$verbraucherstatus(), $verbraucherstatus(),
$warmwasserId, $warmwasserId,
@@ -445,12 +667,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 TestContext $test
) use ( ) use (
$managerId, $managerId,
$netzleistungId, $netzleistungId,
$einStufigId,
$warmwasserId, $warmwasserId,
$pufferspeicherId, $pufferspeicherId,
$melden, $melden,
@@ -471,17 +692,9 @@ return static function (TestContext $test): void {
IPS_RequestAction($managerId, 'Regeln', true); IPS_RequestAction($managerId, 'Regeln', true);
$daten = $verbraucherstatus(); $daten = $verbraucherstatus();
$test->assertSame(false, $daten['Synchronisiert'] ?? null); $test->assertSame(true, $daten['Synchronisiert'] ?? null);
$test->assertSame(2, count($daten['Verbraucher'] ?? [])); $test->assertSame(3, count($daten['Verbraucher'] ?? []));
$test->assertSame(1, count($daten['NichtGeregelt'] ?? [])); $test->assertSame(0, count($daten['NichtGeregelt'] ?? []));
$test->assertSame(
$einStufigId,
$daten['NichtGeregelt'][0]['InstanzID'] ?? null
);
$test->assertSame(
'Meldung ist veraltet',
$daten['NichtGeregelt'][0]['Grund'] ?? null
);
$test->assertTrue( $test->assertTrue(
(float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0 (float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0
); );
+1
View File
@@ -105,6 +105,7 @@ return static function (TestContext $test): void {
$test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId))); $test->assertSame(false, GetValue($test->objectByIdent('Heizbedarf', $verbraucherId)));
SetValue($pufferId, 55.0); SetValue($pufferId, 55.0);
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_RequestAction($verbraucherId, 'Melden', true); IPS_RequestAction($verbraucherId, 'Melden', true);
$test->assertSame([false, false], $kontaktzustand()); $test->assertSame([false, false], $kontaktzustand());
}); });
@@ -225,4 +225,18 @@ return static function (TestContext $test): void {
) === false); ) === false);
$test->assertInstanceStatus($verbraucherId); $test->assertInstanceStatus($verbraucherId);
}); });
$test->runCase('Deaktivierung verwirft eine positive Vorgabe vollstaendig', static function (
TestContext $test
) use ($verbraucherId, $anforderungSenden): void {
IPS_SetProperty($verbraucherId, 'Mindestlaufzeit', 0);
IPS_SetProperty($verbraucherId, 'Mindesteinschaltdauer', 0);
IPS_SetProperty($verbraucherId, 'Mindestausschaltdauer', 0);
IPS_ApplyChanges($verbraucherId);
$anforderungSenden(1200);
$test->assertSame(1200, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_RequestAction($verbraucherId, 'Aktiv', false);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
});
}; };
+8 -3
View File
@@ -22,7 +22,7 @@ return static function (TestContext $test): void {
'Wiederholsperre' => 3, 'Wiederholsperre' => 3,
'Mindestlaufzeit' => 5, 'Mindestlaufzeit' => 5,
'Mindestsperrzeit' => 0, 'Mindestsperrzeit' => 0,
'MaximaleSperrzeit' => 2, 'MaximaleSperrzeit' => 3,
'MaximaleSperrzeit24h' => 60, 'MaximaleSperrzeit24h' => 60,
'VorgabeTimeout' => 30, 'VorgabeTimeout' => 30,
'DiagnosevariablenAnzeigen' => true, 'DiagnosevariablenAnzeigen' => true,
@@ -121,6 +121,8 @@ return static function (TestContext $test): void {
TestContext $test TestContext $test
) use ($waermepumpe, $kontakt1, $kontakt2): void { ) use ($waermepumpe, $kontakt1, $kontakt2): void {
IPS_RequestAction($waermepumpe, 'Aktiv', false); IPS_RequestAction($waermepumpe, 'Aktiv', false);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $waermepumpe)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $waermepumpe)));
IPS_RequestAction($waermepumpe, 'Aktiv', true); IPS_RequestAction($waermepumpe, 'Aktiv', true);
$test->waitUntil( $test->waitUntil(
static fn (): bool => GetValue($kontakt1) === false static fn (): bool => GetValue($kontakt1) === false
@@ -136,13 +138,16 @@ return static function (TestContext $test): void {
) use ($waermepumpe, $kontakt1, $kontakt2, $senden): void { ) use ($waermepumpe, $kontakt1, $kontakt2, $senden): void {
$senden(null, 'Peak'); $senden(null, 'Peak');
$senden(0, 'Peak'); $senden(0, 'Peak');
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === true); $test->waitUntil(static fn (): bool => GetValue($kontakt1) === true, 2500);
$test->assertSame(false, GetValue($kontakt2)); $test->assertSame(false, GetValue($kontakt2));
$test->assertSame(true, GetValue( $test->assertSame(true, GetValue(
$test->objectByIdent('SperreAktiv', $waermepumpe) $test->objectByIdent('SperreAktiv', $waermepumpe)
)); ));
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === false, 2500); $test->waitUntil(static fn (): bool => GetValue($kontakt1) === false, 3500);
$test->waitUntil(static fn (): bool => GetValue(
$test->objectByIdent('SperrerholungAktiv', $waermepumpe)
) === true);
$test->assertSame(true, GetValue( $test->assertSame(true, GetValue(
$test->objectByIdent('SperrerholungAktiv', $waermepumpe) $test->objectByIdent('SperrerholungAktiv', $waermepumpe)
)); ));
@@ -31,6 +31,8 @@ return static function (TestContext $test): void {
'LastwechselSperrzeit' => 1, 'LastwechselSperrzeit' => 1,
'Hysterese' => 5.0, 'Hysterese' => 5.0,
'TemperaturMaxAlter' => 120, 'TemperaturMaxAlter' => 120,
'TemperaturUntergrenze' => 0.0,
'TemperaturObergrenze' => 100.0,
'LegionellenfunktionAktiv' => true, 'LegionellenfunktionAktiv' => true,
'LegionellenMinimalintervallTage' => 4, 'LegionellenMinimalintervallTage' => 4,
'LegionellenMaximalintervallTage' => 7, 'LegionellenMaximalintervallTage' => 7,
@@ -90,6 +92,51 @@ return static function (TestContext $test): void {
IPS_RequestAction($managerId, 'Regeln', true); IPS_RequestAction($managerId, 'Regeln', true);
}; };
$test->runCase('Temperaturgrenzen melden Unter- und Uebertemperatur', static function (
TestContext $test
) use ($verbraucherId, $temperaturSetzen): void {
$stoertextId = $test->objectByIdent('Stoertext', $verbraucherId);
$temperaturSetzen(100.0);
$test->assertTrue(!str_contains((string) GetValue($stoertextId), 'Uebertemperatur'));
$temperaturSetzen(100.1);
$test->assertTrue(str_contains((string) GetValue($stoertextId), 'Uebertemperatur'));
$temperaturSetzen(0.0);
$test->assertTrue(!str_contains((string) GetValue($stoertextId), 'Untertemperatur'));
$temperaturSetzen(-0.1);
$test->assertTrue(str_contains((string) GetValue($stoertextId), 'Untertemperatur'));
$temperaturSetzen(52.0);
});
$test->runCase('Abgelaufene Vorgabe wird auch in der Anzeige verworfen', static function (
TestContext $test
) use ($verbraucherId, $managerId, $kontakte): void {
IPS_SetProperty($verbraucherId, 'VorgabeTimeout', 1);
IPS_ApplyChanges($verbraucherId);
IPS_RequestAction($verbraucherId, 'Aktiv', true);
IPS_RequestAction($verbraucherId, 'ManagerdatenEmpfangen', json_encode([
'Kopf' => [
'Version' => '4.0',
'AbsenderID' => $managerId,
'EmpfaengerID' => $verbraucherId,
'Zeitpunkt' => time(),
],
'Betriebsart' => 'PV',
'Sollleistung_W' => 1000,
], JSON_THROW_ON_ERROR));
$test->assertSame(1000, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
IPS_Sleep(2100);
IPS_RequestAction($verbraucherId, 'Melden', true);
$test->assertSame(false, GetValue($test->objectByIdent('SollwertGueltig', $verbraucherId)));
$test->assertSame(0, GetValue($test->objectByIdent('Sollleistung', $verbraucherId)));
$test->waitUntil(static fn (): bool => GetValue($kontakte[0]) === false);
IPS_RequestAction($verbraucherId, 'Aktiv', false);
IPS_SetProperty($verbraucherId, 'VorgabeTimeout', 120);
IPS_ApplyChanges($verbraucherId);
});
if ($lizenzcode === '') { if ($lizenzcode === '') {
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function ( $test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
TestContext $test TestContext $test
+109
View File
@@ -9,6 +9,86 @@ use PHPUnit\Framework\TestCase;
final class VerbraucherBasisTraitTest extends TestCase final class VerbraucherBasisTraitTest extends TestCase
{ {
/** @dataProvider sollwerte */
public function testVerwerfenLoeschtGespeicherteUndSichtbareVorgabe(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => true];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$modul->verwerfen();
$modul->verwerfen();
self::assertSame(['Sollleistung' => 0, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($sichtbar ? $modul->zustand : [], $modul->sichtbareWerte);
}
/** @dataProvider sollwerte */
public function testUpdateBereinigtAlteUngueltigeVorgaben(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => false];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$modul->bereinigen();
self::assertSame(['Sollleistung' => 0, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($sichtbar ? $modul->zustand : [], $modul->sichtbareWerte);
}
/** @dataProvider sollwerte */
public function testUpdateErhaeltGueltigeVorgabenAuchNullUndEntladen(int $leistung, bool $sichtbar): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => $leistung, 'SollwertGueltig' => true];
$modul->sichtbareWerte = $sichtbar ? $modul->zustand : [];
$vorher = $modul->zustand;
$modul->bereinigen();
self::assertSame($vorher, $modul->zustand);
self::assertSame($sichtbar ? $vorher : [], $modul->sichtbareWerte);
}
public static function sollwerte(): array
{
return [[10944, true], [10944, false], [-843, true], [-843, false], [0, true], [0, false]];
}
public function testWiederholtesVerwerfenLaesstNeuBerechneteLokaleSchutzleistungStehen(): void
{
$modul = new VerbraucherBasisDummy();
$modul->zustand = ['Sollleistung' => 3000, 'SollwertGueltig' => false];
$modul->sichtbareWerte = $modul->zustand;
$modul->verwerfen();
self::assertSame(['Sollleistung' => 3000, 'SollwertGueltig' => false], $modul->zustand);
self::assertSame($modul->zustand, $modul->sichtbareWerte);
}
public function testAlleVerbraucherVerwerfenSollwerteUeberDieGemeinsameFunktion(): void
{
$anzahl = 0;
foreach (glob(__DIR__ . '/../*/module.php') as $datei) {
$quelle = (string) file_get_contents($datei);
if (!str_contains($quelle, 'implements VerbraucherSchnittstelle')) {
continue;
}
++$anzahl;
self::assertStringContainsString('use VerbraucherBasisTrait;', $quelle, $datei);
self::assertStringContainsString('$this->verwerfeSollwert();', $quelle, $datei);
self::assertStringNotContainsString("setzeZustand('SollwertGueltig', false)", $quelle, $datei);
self::assertStringNotContainsString("WriteAttributeBoolean('ZustandSollwertGueltig', false)", $quelle, $datei);
self::assertStringContainsString(
'$this->aktualisiereVariablen();' . "\n " . '$this->bereinigeUngueltigenSollwert();',
$quelle,
$datei
);
}
self::assertSame(7, $anzahl);
}
public function testGemeinsamePropertiesUndVariablenWerdenRegistriert(): void public function testGemeinsamePropertiesUndVariablenWerdenRegistriert(): void
{ {
$modul = new VerbraucherBasisDummy(); $modul = new VerbraucherBasisDummy();
@@ -50,6 +130,35 @@ final class VerbraucherBasisDummy
{ {
use VerbraucherBasisTrait; use VerbraucherBasisTrait;
/** @var array<string, int|bool> */
public array $zustand = [];
/** @var array<string, int|bool> */
public array $sichtbareWerte = [];
public function verwerfen(): void
{
$this->verwerfeSollwert();
}
public function bereinigen(): void
{
$this->bereinigeUngueltigenSollwert();
}
private function setzeZustand(string $ident, $wert): void
{
$this->zustand[$ident] = $wert;
if (array_key_exists($ident, $this->sichtbareWerte)) {
$this->sichtbareWerte[$ident] = $wert;
}
}
private function leseZustand(string $ident)
{
return $this->zustand[$ident];
}
/** @var array<string, mixed> */ /** @var array<string, mixed> */
public array $properties = []; public array $properties = [];
+34 -2
View File
@@ -29,14 +29,14 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']); self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
} }
public function testAlle19PropertiesSindRegistriertUndImFormularDokumentiert(): void public function testAlle21PropertiesSindRegistriertUndImFormularDokumentiert(): void
{ {
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php'); $basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php'); $modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer); preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer);
$properties = array_values(array_unique($treffer[1])); $properties = array_values(array_unique($treffer[1]));
self::assertCount(19, $properties); self::assertCount(21, $properties);
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json'); $formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
foreach ($properties as $property) { foreach ($properties as $property) {
@@ -136,6 +136,38 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase
self::assertStringContainsString('Mindesttemperatur < Maximaltemperatur', $inhalt); self::assertStringContainsString('Mindesttemperatur < Maximaltemperatur', $inhalt);
} }
public function testTemperaturstoergrenzenSindKonfigurierbarUndVonSollwertenGetrennt(): void
{
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
self::assertStringContainsString(
"RegisterPropertyFloat('TemperaturUntergrenze', 0.0)",
$inhalt
);
self::assertStringContainsString(
"RegisterPropertyFloat('TemperaturObergrenze', 100.0)",
$inhalt
);
self::assertStringContainsString("ReadPropertyFloat('TemperaturUntergrenze')", $inhalt);
self::assertStringContainsString("ReadPropertyFloat('TemperaturObergrenze')", $inhalt);
self::assertStringContainsString("'Kennung' => 'Untertemperatur'", $inhalt);
self::assertStringContainsString("'Kennung' => 'Uebertemperatur'", $inhalt);
$formular = json_decode(
(string) file_get_contents(self::MODULPFAD . 'form.json'),
true,
512,
JSON_THROW_ON_ERROR
);
$erweitert = array_values(array_filter(
$formular['elements'],
static fn (array $element): bool =>
$element['caption'] === 'Erweiterte Speichereinstellungen'
));
self::assertCount(1, $erweitert);
self::assertContains('TemperaturUntergrenze', array_column($erweitert[0]['items'], 'name'));
self::assertContains('TemperaturObergrenze', array_column($erweitert[0]['items'], 'name'));
}
public function testSchaltfolgeIstBreakBeforeMake(): void public function testSchaltfolgeIstBreakBeforeMake(): void
{ {
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php'); $inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');