feat(ladestation): Phasenerkennung und Schaltsperren erweitern
Tests / test (push) Successful in 45s

This commit is contained in:
dh
2026-09-29 22:49:26 +00:00
parent 53fca5e2bb
commit faa5dfb33f
10 changed files with 593 additions and 113 deletions
+28
View File
@@ -38,12 +38,40 @@
"suffix": " s", "suffix": " s",
"minimum": 1 "minimum": 1
}, },
{
"type": "NumberSpinner",
"name": "Phasenerkennungszeit",
"caption": "Phasen erkennen nach",
"suffix": " s",
"minimum": 1
},
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "FahrzeugstromErkennungszeit", "name": "FahrzeugstromErkennungszeit",
"caption": "Fahrzeug-Maximalstrom erkennen nach", "caption": "Fahrzeug-Maximalstrom erkennen nach",
"suffix": " s", "suffix": " s",
"minimum": 30 "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
} }
] ]
}, },
+181 -5
View File
@@ -51,7 +51,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('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);
@@ -89,6 +93,12 @@ 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->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->RegisterAttributeInteger('ErkannterMaximalstrom', 32); $this->RegisterAttributeInteger('ErkannterMaximalstrom', 32);
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0); $this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
@@ -126,6 +136,18 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
// 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->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->WriteAttributeInteger( $this->WriteAttributeInteger(
'ErkannterMaximalstrom', 'ErkannterMaximalstrom',
@@ -283,6 +305,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);
@@ -290,7 +346,14 @@ 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);
$this->setzeLadestrom($sollleistung); if (!($aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
)) {
$this->setzeLadestrom($sollleistung);
}
$this->setzeStoerung(''); $this->setzeStoerung('');
$this->SetStatus(self::STATUS_AKTIV); $this->SetStatus(self::STATUS_AKTIV);
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
@@ -319,19 +382,73 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->ReadPropertyInteger('Geraetetyp'), $this->ReadPropertyInteger('Geraetetyp'),
$antwort $antwort
); );
$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(
$this->ReadPropertyInteger('MaximalerLadestrom')
);
$erkennungSeit = time();
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
} elseif ((time() - $erkennungSeit)
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
) {
$bekanntePhasenzahl = $rohstatus['Istleistung_W'] > 7500.0
? LadestationRegler::PHASEN_DREI
: LadestationRegler::PHASEN_EIN;
$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') $this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt'),
$bekanntePhasenzahl
); );
if (!$status['FahrzeugVerbunden']) { if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false); $this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
} elseif ($status['Ladestrom_A'] >= 3.5) { } elseif ($status['Ladestrom_A'] >= 3.5) {
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true); $this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true);
} }
$this->aktualisiereFahrzeugstromgrenze($status); 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);
@@ -374,6 +491,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
} }
$this->WriteAttributeInteger('ErkannterMaximalstrom', $kandidat); $this->WriteAttributeInteger('ErkannterMaximalstrom', $kandidat);
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $kandidat); $this->setzeSichtbareVariable('FahrzeugMaximalstrom', $kandidat);
$this->setzeFahrzeugstromKandidatZurueck(); $this->setzeFahrzeugstromKandidatZurueck();
} }
@@ -416,9 +534,56 @@ 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; return;
} }
if (!$stromUnveraendert) {
$this->sendeLadestromAnGeraet($strom);
}
if ($leistung === $bisherigeLeistung) {
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->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
$this->setzeFahrzeugstromKandidatZurueck(); $this->setzeFahrzeugstromKandidatZurueck();
@@ -660,12 +825,23 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
'Meldeintervall', 'Meldeintervall',
'VorgabeTimeout', 'VorgabeTimeout',
'Abfrageintervall', 'Abfrageintervall',
'Phasenerkennungszeit',
'FahrzeugstromErkennungszeit', 'FahrzeugstromErkennungszeit',
] as $property) { ] 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.');
+51 -22
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 den konfigurierten Maximalstrom als Erkennungsstrom vor. Bis
5. War das Fahrzeug bereits im vorherigen Zyklus verbunden und liegt der zur abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot
ermittelte Maximalstrom aus Ladestrom plus `2.5 A` unter `6 A`, wird `[0]`.
`FahrzeugGeladen=true` gesetzt. 5. Nach `Phasenerkennungszeit` gilt wie in Enelix 1: ueber 7500 W werden drei
Phasen erkannt, andernfalls eine Phase. 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,18 @@ 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.
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
@@ -114,8 +129,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 Maximalstrom-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. |
@@ -232,16 +252,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 konfigurierte Maximalstrom zur Erkennung angefordert.
- Bei einer Ladeleistung bis 7500 W wird eine Phase, darueber werden drei - Nach der Erkennungszeit werden bis 7500 W eine Phase, darueber drei Phasen
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 +282,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 +298,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 Maximalstrom-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:
+14 -6
View File
@@ -91,7 +91,7 @@ final class LadestationAdapter
} }
/** /**
* @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float} * @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int}
*/ */
public static function dekodiereStatus(int $geraetetyp, string $antwort): array public static function dekodiereStatus(int $geraetetyp, string $antwort): array
{ {
@@ -121,11 +121,19 @@ final class LadestationAdapter
&& is_numeric($daten['nrg'][6]) && is_numeric($daten['nrg'][6])
) { ) {
$stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0; $stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0;
$status['Iststrom_A'] = max( $phasenstroeme = [
(float) $daten['nrg'][4], (float) $daten['nrg'][4] * $stromfaktor,
(float) $daten['nrg'][5], (float) $daten['nrg'][5] * $stromfaktor,
(float) $daten['nrg'][6] (float) $daten['nrg'][6] * $stromfaktor,
) * $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; return $status;
+36 -25
View File
@@ -12,27 +12,15 @@ 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> */
private const LEISTUNGEN_DREIPHASIG = [
4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700,
10300, 11000, 11750, 12450, 13150, 13850, 14550, 15250,
15900, 16600, 17300, 18000, 18700, 19400, 20100, 20800,
21500, 22000,
];
/** /**
* Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation. * Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
* Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit * Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
* die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt. * die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt.
* *
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: 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(
@@ -40,7 +28,8 @@ final class LadestationRegler
bool $warVerbunden, bool $warVerbunden,
bool $warGeladen = false, bool $warGeladen = false,
bool $ladungErwartet = true, bool $ladungErwartet = true,
bool $ladeleistungWurdeErkannt = false 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']);
@@ -49,11 +38,22 @@ 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;
} 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']) $ladestrom = isset($rohstatus['Iststrom_A'])
? max(0.0, (float) $rohstatus['Iststrom_A']) ? max(0.0, (float) $rohstatus['Iststrom_A'])
: $leistung / $nenner; : ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
$ermittelterMaximalstrom = 2.5 + $ladestrom; $ermittelterMaximalstrom = 2.5 + $ladestrom;
$geladen = $warGeladen $geladen = $warGeladen
|| ($warVerbunden || ($warVerbunden
@@ -94,7 +94,10 @@ final class LadestationRegler
return null; return null;
} }
$kandidat = max(6, min($konfigurierterMaximalstrom, (int) round($gemessenerStrom))); $kandidat = max(
6,
min($konfigurierterMaximalstrom, (int) floor($gemessenerStrom + 2.5))
);
if ($kandidat > $vorgegebenerStrom - 2 || $kandidat >= $konfigurierterMaximalstrom) { if ($kandidat > $vorgegebenerStrom - 2 || $kandidat >= $konfigurierterMaximalstrom) {
return null; return null;
} }
@@ -117,6 +120,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];
@@ -167,12 +174,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)
);
} }
} }
+13 -3
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, Iststrom_A?: float} $erwartet * @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $erwartet
*/ */
public function testStatusaufrufUndDekodierungFuerAlleGeraete( public function testStatusaufrufUndDekodierungFuerAlleGeraete(
int $geraetetyp, int $geraetetyp,
@@ -49,14 +49,24 @@ final class LadestationAdapterTest extends TestCase
$this->lokaleKonfiguration(), $this->lokaleKonfiguration(),
$this->goEAntwort(2, 1100, [160, 158, 159]), $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, 'Iststrom_A' => 16.0], [
'Rohstatus' => 2,
'Istleistung_W' => 11000.0,
'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, [9.8, 0, 0]), $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, 'Iststrom_A' => 9.8], [
'Rohstatus' => 2,
'Istleistung_W' => 2300.0,
'Iststrom_A' => 9.8,
'Phasenzahl' => 1,
],
]; ];
yield 'Smart-me Pico' => [ yield 'Smart-me Pico' => [
LadestationAdapter::SMART_ME_PICO, LadestationAdapter::SMART_ME_PICO,
+37 -5
View File
@@ -22,17 +22,49 @@ 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 testErkannteDreiphasigkeitBleibtWaehrendLadepauseErhalten(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], 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 public function testAnlaufOhneGemesseneLadeleistungGiltNichtAlsLadeende(): void
{ {
$status = LadestationRegler::normalisiereStatus([ $status = LadestationRegler::normalisiereStatus([
@@ -90,7 +122,7 @@ final class LadestationReglerTest extends TestCase
public function testFahrzeugMaximalstromWirdMitZweiAmpereAbstandErkannt(): void public function testFahrzeugMaximalstromWirdMitZweiAmpereAbstandErkannt(): void
{ {
self::assertSame(16, LadestationRegler::ermittleFahrzeugMaximalstrom(20, 15.9, 20)); self::assertSame(18, LadestationRegler::ermittleFahrzeugMaximalstrom(20, 15.9, 20));
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(17, 15.9, 20)); self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(17, 15.9, 20));
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(20, 0.0, 20)); self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(20, 0.0, 20));
} }
@@ -104,7 +136,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);
} }
@@ -112,7 +144,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,6 +68,22 @@ 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);
+2 -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)
) )
+215 -45
View File
@@ -50,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,
@@ -71,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,
@@ -88,12 +90,85 @@ return static function (TestContext $test): void {
'Geraeteadresse' => '192.0.2.13', 'Geraeteadresse' => '192.0.2.13',
'MaximalerLadestrom' => 20, 'MaximalerLadestrom' => 20,
'FahrzeugstromErkennungszeit' => 1, 'FahrzeugstromErkennungszeit' => 1,
'ZeitZwischenZustandswechseln' => 0,
'Ladefreigabe' => true, 'Ladefreigabe' => true,
'Solarladen' => true, 'Solarladen' => true,
'EinstellungenInVisu' => true, 'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true, 'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true, 'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 10263, [15.9, 15.8, 15.7]), '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]),
] ]
); );
@@ -104,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_merge(array_values($stationen), [$stromerkennung]) array_merge(
array_values($stationen),
[
$stromerkennung,
$phasenerkennung,
$phasenhaltung,
$schaltsperre,
$mindestzeiten,
]
)
), JSON_THROW_ON_ERROR), ), JSON_THROW_ON_ERROR),
] ]
); );
@@ -156,6 +240,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 {
@@ -171,35 +343,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, [100, 100, 100]));
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)),
@@ -214,22 +363,22 @@ return static function (TestContext $test): void {
IPS_RequestAction($stationId, 'Solarladen', false); IPS_RequestAction($stationId, 'Solarladen', false);
$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(
@@ -254,37 +403,58 @@ return static function (TestContext $test): void {
) use ($stromerkennung, $anforderungSenden): void { ) use ($stromerkennung, $anforderungSenden): void {
IPS_RequestAction($stromerkennung, 'Aktiv', true); IPS_RequestAction($stromerkennung, 'Aktiv', true);
IPS_RequestAction($stromerkennung, 'Solarladen', true); IPS_RequestAction($stromerkennung, 'Solarladen', true);
$anforderungSenden($stromerkennung, 13850); $anforderungSenden($stromerkennung, 13680);
IPS_RequestAction($stromerkennung, 'Statuszyklus', false); IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
IPS_Sleep(2100); IPS_Sleep(2100);
IPS_RequestAction($stromerkennung, 'Statuszyklus', false); IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
$test->assertSame( $test->assertSame(
16, 18,
GetValue($test->objectByIdent('FahrzeugMaximalstrom', $stromerkennung)) GetValue($test->objectByIdent('FahrzeugMaximalstrom', $stromerkennung))
); );
$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,11628,12312]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stromerkennung)) (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stromerkennung))
); );
$test->assertTrue(str_contains( $test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stromerkennung)), (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stromerkennung)),
'amp=16' 'amp=18'
)); ));
}); });
$test->runCase('Sollwert bleibt waehrend der Ladeanlaufzeit gueltig', static function ( $test->runCase('Neue Ladestation wird erst nach Freigabe mit Maximalstrom erkannt', static function (
TestContext $test TestContext $test
) use ($goEAntwort, $stationen, $anforderungSenden): void { ) use ($phasenerkennung, $anforderungSenden): void {
$stationId = $stationen['go-e Gemini']; $stationId = $phasenerkennung;
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 0)); $test->assertSame(
IPS_ApplyChanges($stationId); 0,
IPS_RequestAction($stationId, 'Aktiv', true); GetValue($test->objectByIdent('Phasenzahl', $stationId))
IPS_RequestAction($stationId, 'Solarladen', true); );
$test->assertTrue(!str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=16'
), 'Eine inaktive Station darf keine Maximalstrom-Probe starten.');
$anforderungSenden($stationId, 3850); IPS_RequestAction($stationId, 'Aktiv', true);
$test->assertSame(
0,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=16'
), 'Nach der Freigabe muss die Maximalstrom-Probe starten.');
IPS_Sleep(2100);
IPS_RequestAction($stationId, 'Statuszyklus', false);
$anforderungSenden($stationId, 3680);
IPS_RequestAction($stationId, 'Statuszyklus', false); IPS_RequestAction($stationId, 'Statuszyklus', false);
$test->assertSame(
1,
GetValue($test->objectByIdent('Phasenzahl', $stationId))
);
$test->assertSame( $test->assertSame(
false, false,
GetValue($test->objectByIdent('FahrzeugGeladen', $stationId)) GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))
@@ -294,11 +464,11 @@ return static function (TestContext $test): void {
GetValue($test->objectByIdent('SollwertGueltig', $stationId)) GetValue($test->objectByIdent('SollwertGueltig', $stationId))
); );
$test->assertSame( $test->assertSame(
3850, 3680,
GetValue($test->objectByIdent('Sollleistung', $stationId)) GetValue($test->objectByIdent('Sollleistung', $stationId))
); );
$test->assertSame( $test->assertSame(
'[0,1450,1690,1930,2170,2410,2650,2890,3130,3370,3610,3850]', '[0,1380,1610,1840,2070,2300,2530,2760,2990,3220,3450,3680]',
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId)) (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
); );
}); });