feat(ladestation): Phasenerkennung und Schaltsperren erweitern
Tests / test (push) Successful in 45s
Tests / test (push) Successful in 45s
This commit is contained in:
@@ -38,12 +38,40 @@
|
||||
"suffix": " s",
|
||||
"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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -51,7 +51,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->RegisterPropertyString('Benutzername', '');
|
||||
$this->RegisterPropertyString('Passwort', '');
|
||||
$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->RegisterPropertyBoolean('Ladefreigabe', true);
|
||||
$this->RegisterPropertyBoolean('Solarladen', true);
|
||||
@@ -89,6 +93,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->RegisterAttributeInteger('LetzteManagerID', 0);
|
||||
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
|
||||
$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('ErkannterMaximalstrom', 32);
|
||||
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
|
||||
@@ -126,6 +136,18 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
// Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen.
|
||||
$this->setzeZustand('FahrzeugGeladen', false);
|
||||
$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',
|
||||
@@ -283,6 +305,40 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$solarladen,
|
||||
$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);
|
||||
$this->WriteAttributeString('Leistungsangebot', $angebotJson);
|
||||
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
|
||||
@@ -290,7 +346,14 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
|
||||
|
||||
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
||||
$this->setzeLadestrom($sollleistung);
|
||||
if (!($aktiv
|
||||
&& $ladefreigabe
|
||||
&& $verbunden
|
||||
&& !$geladen
|
||||
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
|
||||
)) {
|
||||
$this->setzeLadestrom($sollleistung);
|
||||
}
|
||||
$this->setzeStoerung('');
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
} catch (Throwable $fehler) {
|
||||
@@ -319,19 +382,73 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$this->ReadPropertyInteger('Geraetetyp'),
|
||||
$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(
|
||||
$rohstatus,
|
||||
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'),
|
||||
$warVerbunden,
|
||||
$this->ReadAttributeBoolean('ZustandFahrzeugGeladen'),
|
||||
$this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6,
|
||||
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt')
|
||||
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt'),
|
||||
$bekanntePhasenzahl
|
||||
);
|
||||
if (!$status['FahrzeugVerbunden']) {
|
||||
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||
} elseif ($status['Ladestrom_A'] >= 3.5) {
|
||||
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true);
|
||||
}
|
||||
$this->aktualisiereFahrzeugstromgrenze($status);
|
||||
if (!$status['FahrzeugVerbunden']
|
||||
|| $status['Phasenzahl'] !== LadestationRegler::PHASEN_UNBEKANNT
|
||||
) {
|
||||
$this->aktualisiereFahrzeugstromgrenze($status);
|
||||
}
|
||||
|
||||
foreach ($status as $ident => $wert) {
|
||||
$this->setzeZustand($ident, $wert);
|
||||
@@ -374,6 +491,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
}
|
||||
|
||||
$this->WriteAttributeInteger('ErkannterMaximalstrom', $kandidat);
|
||||
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
|
||||
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $kandidat);
|
||||
$this->setzeFahrzeugstromKandidatZurueck();
|
||||
}
|
||||
@@ -416,9 +534,56 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
$strom = $leistung === 0
|
||||
? 0
|
||||
: 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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -660,12 +825,23 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
||||
'Meldeintervall',
|
||||
'VorgabeTimeout',
|
||||
'Abfrageintervall',
|
||||
'Phasenerkennungszeit',
|
||||
'FahrzeugstromErkennungszeit',
|
||||
] as $property) {
|
||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||
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');
|
||||
if ($maximalstrom < 6 || $maximalstrom > 32) {
|
||||
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||
|
||||
@@ -40,13 +40,20 @@ Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
|
||||
wird entsprechend `State != 1` ausgewertet.
|
||||
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.
|
||||
3. Bei einer Leistung ueber 7500 W wird dreiphasiges Laden erkannt. Darunter
|
||||
wird bei verbundenem Fahrzeug einphasiges Laden angenommen.
|
||||
4. Der Ladestrom wird aus der Leistung mit `230 V` einphasig beziehungsweise
|
||||
`1.71 * 400 V` dreiphasig berechnet.
|
||||
5. War das Fahrzeug bereits im vorherigen Zyklus verbunden und liegt der
|
||||
ermittelte Maximalstrom aus Ladestrom plus `2.5 A` unter `6 A`, wird
|
||||
`FahrzeugGeladen=true` gesetzt.
|
||||
3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
|
||||
Strom fuehren, wird dreiphasiges Laden direkt erkannt.
|
||||
4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
|
||||
Modul zunaechst den konfigurierten Maximalstrom als Erkennungsstrom vor. Bis
|
||||
zur abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot
|
||||
`[0]`.
|
||||
5. Nach `Phasenerkennungszeit` gilt wie in Enelix 1: ueber 7500 W werden drei
|
||||
Phasen erkannt, andernfalls eine Phase. Die erkannte Phasenzahl bleibt bis
|
||||
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:
|
||||
|
||||
@@ -72,10 +79,18 @@ stoppt die Ladestation.
|
||||
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
|
||||
| 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
|
||||
Enelix 1 uebernommen. Ohne gueltige Manager-Vorgabe faehrt das Modul bei
|
||||
aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es die
|
||||
maximale angebotene Leistung.
|
||||
Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
|
||||
`684 W` pro Ampere dreiphasig. Ohne gueltige Manager-Vorgabe faehrt das Modul
|
||||
bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
|
||||
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
|
||||
|
||||
@@ -114,8 +129,13 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
||||
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
||||
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
||||
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
||||
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der Maximalstrom-Probe vor der leistungsbasierten Phasenerkennung. |
|
||||
| `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. |
|
||||
| `Ladefreigabe` | Boolean | `false` | Startwert der lokalen Ladefreigabe. |
|
||||
| `Ladefreigabe` | Boolean | `true` | Startwert der lokalen Ladefreigabe. |
|
||||
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
|
||||
| `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`.
|
||||
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
||||
Leistungsangebot `[0]`.
|
||||
- Nach dem Anstecken wird das Fahrzeug erkannt und die gemessene Leistung
|
||||
plausibel in `Istleistung` und `Ladestrom` abgebildet.
|
||||
- Bei einer Ladeleistung bis 7500 W wird eine Phase, darueber werden drei
|
||||
Phasen angezeigt.
|
||||
- Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
|
||||
zunaechst der konfigurierte Maximalstrom zur Erkennung angefordert.
|
||||
- Nach der Erkennungszeit werden bis 7500 W eine Phase, darueber drei Phasen
|
||||
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.
|
||||
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen 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=false` bietet `0 W` und die Ladestufen an.
|
||||
- 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.
|
||||
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
|
||||
Zugangsdaten.
|
||||
@@ -256,9 +282,11 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
|
||||
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
|
||||
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
|
||||
`ActiveChargingPower`.
|
||||
- Falsche Phasenzahl bei sehr kleiner oder stehender Ladung: Die kompatible
|
||||
Enelix-1-Erkennung basiert auf der momentanen Leistung. Fuer eine sichere
|
||||
Dreiphasenerkennung muss waehrend der Erkennung mehr als 7500 W anliegen.
|
||||
- Phasenzahl bleibt laenger auf `0`: Waehrend der Erkennungsphase muessen
|
||||
Ladestation und Fahrzeug den angeforderten Maximalstrom tatsaechlich
|
||||
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
|
||||
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
|
||||
|
||||
@@ -270,9 +298,10 @@ URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
|
||||
geprueft.
|
||||
|
||||
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, Fahrzeug- und
|
||||
Phasenerkennung, die Leistungsangebote in PV und Peak sowie die Umschaltung
|
||||
ueber den `Solarladen`-Button.
|
||||
API-Antworten. Er prueft alle drei Geraetevarianten, die Maximalstrom-Probe
|
||||
beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
|
||||
Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
|
||||
Umschaltung ueber den `Solarladen`-Button.
|
||||
|
||||
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -121,11 +121,19 @@ final class LadestationAdapter
|
||||
&& is_numeric($daten['nrg'][6])
|
||||
) {
|
||||
$stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0;
|
||||
$status['Iststrom_A'] = max(
|
||||
(float) $daten['nrg'][4],
|
||||
(float) $daten['nrg'][5],
|
||||
(float) $daten['nrg'][6]
|
||||
) * $stromfaktor;
|
||||
$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;
|
||||
|
||||
+36
-25
@@ -12,27 +12,15 @@ final class LadestationRegler
|
||||
public const PHASEN_EIN = 1;
|
||||
public const PHASEN_DREI = 3;
|
||||
|
||||
/** @var list<int> */
|
||||
private const LEISTUNGEN_EINPHASIG = [
|
||||
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,
|
||||
];
|
||||
private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
|
||||
private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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>
|
||||
*/
|
||||
public static function normalisiereStatus(
|
||||
@@ -40,7 +28,8 @@ final class LadestationRegler
|
||||
bool $warVerbunden,
|
||||
bool $warGeladen = false,
|
||||
bool $ladungErwartet = true,
|
||||
bool $ladeleistungWurdeErkannt = false
|
||||
bool $ladeleistungWurdeErkannt = false,
|
||||
int $bekanntePhasenzahl = self::PHASEN_UNBEKANNT
|
||||
): array {
|
||||
$verbunden = $rohstatus['Rohstatus'] !== 1;
|
||||
$leistung = max(0.0, $rohstatus['Istleistung_W']);
|
||||
@@ -49,11 +38,22 @@ final class LadestationRegler
|
||||
$geladen = false;
|
||||
|
||||
if ($verbunden) {
|
||||
$phasen = $leistung > 7500.0 ? self::PHASEN_DREI : self::PHASEN_EIN;
|
||||
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0;
|
||||
if (in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)) {
|
||||
$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'])
|
||||
? max(0.0, (float) $rohstatus['Iststrom_A'])
|
||||
: $leistung / $nenner;
|
||||
: ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
|
||||
$ermittelterMaximalstrom = 2.5 + $ladestrom;
|
||||
$geladen = $warGeladen
|
||||
|| ($warVerbunden
|
||||
@@ -94,7 +94,10 @@ final class LadestationRegler
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -117,6 +120,10 @@ final class LadestationRegler
|
||||
return [0];
|
||||
}
|
||||
|
||||
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
|
||||
return [0];
|
||||
}
|
||||
|
||||
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
|
||||
if ($stufen === []) {
|
||||
return [0];
|
||||
@@ -167,12 +174,16 @@ final class LadestationRegler
|
||||
private static function stufenFuerPhasen(int $phasenzahl): array
|
||||
{
|
||||
if ($phasenzahl === self::PHASEN_EIN) {
|
||||
return self::LEISTUNGEN_EINPHASIG;
|
||||
}
|
||||
if ($phasenzahl === self::PHASEN_DREI) {
|
||||
return self::LEISTUNGEN_DREIPHASIG;
|
||||
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_EINPHASIG;
|
||||
} elseif ($phasenzahl === self::PHASEN_DREI) {
|
||||
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_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,7 +13,7 @@ final class LadestationAdapterTest extends TestCase
|
||||
/**
|
||||
* @dataProvider statusFaelle
|
||||
* @param array<string, string> $konfiguration
|
||||
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float} $erwartet
|
||||
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $erwartet
|
||||
*/
|
||||
public function testStatusaufrufUndDekodierungFuerAlleGeraete(
|
||||
int $geraetetyp,
|
||||
@@ -49,14 +49,24 @@ final class LadestationAdapterTest extends TestCase
|
||||
$this->lokaleKonfiguration(),
|
||||
$this->goEAntwort(2, 1100, [160, 158, 159]),
|
||||
'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' => [
|
||||
LadestationAdapter::GO_E_GEMINI,
|
||||
$this->lokaleKonfiguration(),
|
||||
$this->goEAntwort(2, 2300, [9.8, 0, 0]),
|
||||
'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' => [
|
||||
LadestationAdapter::SMART_ME_PICO,
|
||||
|
||||
@@ -22,17 +22,49 @@ final class LadestationReglerTest extends TestCase
|
||||
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
|
||||
}
|
||||
|
||||
public function testEinphasigesFahrzeugWirdUnterhalb7500WErkannt(): void
|
||||
public function testDirektGemeldeteEinphasigkeitWirdAuchUnter7500WErkannt(): void
|
||||
{
|
||||
$status = LadestationRegler::normalisiereStatus([
|
||||
'Rohstatus' => 2,
|
||||
'Istleistung_W' => 2300.0,
|
||||
'Phasenzahl' => 1,
|
||||
], false);
|
||||
|
||||
self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']);
|
||||
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
|
||||
{
|
||||
$status = LadestationRegler::normalisiereStatus([
|
||||
@@ -90,7 +122,7 @@ final class LadestationReglerTest extends TestCase
|
||||
|
||||
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(20, 0.0, 20));
|
||||
}
|
||||
@@ -104,7 +136,7 @@ final class LadestationReglerTest extends TestCase
|
||||
|
||||
self::assertSame(0, $solarPV[0]);
|
||||
self::assertCount(12, $solarPV);
|
||||
self::assertSame([3850], $normalPV);
|
||||
self::assertSame([3680], $normalPV);
|
||||
self::assertSame([0], $solarPeak);
|
||||
self::assertSame($solarPV, $normalPeak);
|
||||
}
|
||||
@@ -112,7 +144,7 @@ final class LadestationReglerTest extends TestCase
|
||||
public function testPeakKannJedesFahrzeugAufNullSetzen(): void
|
||||
{
|
||||
self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3));
|
||||
self::assertSame(6, LadestationRegler::stromFuerLeistung(4150, 3));
|
||||
self::assertSame(16, LadestationRegler::stromFuerLeistung(11000, 3));
|
||||
self::assertSame(6, LadestationRegler::stromFuerLeistung(4104, 3));
|
||||
self::assertSame(16, LadestationRegler::stromFuerLeistung(10944, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,22 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
|
||||
"RegisterPropertyBoolean('Ladefreigabe', true)",
|
||||
$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('Solarladen')", $modul);
|
||||
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||
|
||||
@@ -95,7 +95,7 @@ return static function (TestContext $test): void {
|
||||
) use ($station): void {
|
||||
IPS_RequestAction($station, 'Aktiv', true);
|
||||
$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', $station)
|
||||
)
|
||||
@@ -125,7 +125,7 @@ return static function (TestContext $test): void {
|
||||
);
|
||||
$test->assertInstanceStatus($station);
|
||||
$test->assertSame(
|
||||
'[3850]',
|
||||
'[3680]',
|
||||
(string) GetValue(
|
||||
$test->objectByIdent('LeistungsangebotDiagnose', $station)
|
||||
)
|
||||
|
||||
@@ -50,6 +50,7 @@ return static function (TestContext $test): void {
|
||||
foreach ($definitionen as $name => $definition) {
|
||||
$properties = array_merge($definition, [
|
||||
'MaximalerLadestrom' => 16,
|
||||
'ZeitZwischenZustandswechseln' => 0,
|
||||
'Ladefreigabe' => true,
|
||||
'Solarladen' => true,
|
||||
'EinstellungenInVisu' => true,
|
||||
@@ -71,6 +72,7 @@ return static function (TestContext $test): void {
|
||||
'Geraetetyp' => 1,
|
||||
'Geraeteadresse' => '192.0.2.12',
|
||||
'MaximalerLadestrom' => 16,
|
||||
'ZeitZwischenZustandswechseln' => 0,
|
||||
'Ladefreigabe' => true,
|
||||
'Solarladen' => true,
|
||||
'EinstellungenInVisu' => true,
|
||||
@@ -88,12 +90,85 @@ return static function (TestContext $test): void {
|
||||
'Geraeteadresse' => '192.0.2.13',
|
||||
'MaximalerLadestrom' => 20,
|
||||
'FahrzeugstromErkennungszeit' => 1,
|
||||
'ZeitZwischenZustandswechseln' => 0,
|
||||
'Ladefreigabe' => true,
|
||||
'Solarladen' => true,
|
||||
'EinstellungenInVisu' => true,
|
||||
'DiagnosevariablenAnzeigen' => 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,
|
||||
'VerbraucherZuordnung' => json_encode(array_map(
|
||||
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),
|
||||
]
|
||||
);
|
||||
@@ -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 (
|
||||
TestContext $test
|
||||
) 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 (
|
||||
TestContext $test
|
||||
) use ($stationen, $anforderungSenden): void {
|
||||
$stationId = $stationen['go-e alt'];
|
||||
IPS_RequestAction($stationId, 'Solarladen', true);
|
||||
$anforderungSenden($stationId, 4150);
|
||||
$anforderungSenden($stationId, 4104);
|
||||
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||
$diagnose = json_encode([
|
||||
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)),
|
||||
@@ -214,22 +363,22 @@ return static function (TestContext $test): void {
|
||||
|
||||
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||
$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;
|
||||
try {
|
||||
$anforderungSenden($stationId, 4150);
|
||||
$anforderungSenden($stationId, 4104);
|
||||
} catch (Throwable $fehler) {
|
||||
$abgewiesen = true;
|
||||
}
|
||||
if (!$abgewiesen) {
|
||||
$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.');
|
||||
|
||||
$anforderungSenden($stationId, 4150, 'Peak');
|
||||
$anforderungSenden($stationId, 4104, 'Peak');
|
||||
$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))
|
||||
);
|
||||
$test->assertTrue(str_contains(
|
||||
@@ -254,37 +403,58 @@ return static function (TestContext $test): void {
|
||||
) use ($stromerkennung, $anforderungSenden): void {
|
||||
IPS_RequestAction($stromerkennung, 'Aktiv', true);
|
||||
IPS_RequestAction($stromerkennung, 'Solarladen', true);
|
||||
$anforderungSenden($stromerkennung, 13850);
|
||||
$anforderungSenden($stromerkennung, 13680);
|
||||
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
|
||||
IPS_Sleep(2100);
|
||||
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
|
||||
|
||||
$test->assertSame(
|
||||
16,
|
||||
18,
|
||||
GetValue($test->objectByIdent('FahrzeugMaximalstrom', $stromerkennung))
|
||||
);
|
||||
$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))
|
||||
);
|
||||
$test->assertTrue(str_contains(
|
||||
(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
|
||||
) use ($goEAntwort, $stationen, $anforderungSenden): void {
|
||||
$stationId = $stationen['go-e Gemini'];
|
||||
IPS_SetProperty($stationId, 'Testantwort', $goEAntwort(2, 0));
|
||||
IPS_ApplyChanges($stationId);
|
||||
IPS_RequestAction($stationId, 'Aktiv', true);
|
||||
IPS_RequestAction($stationId, 'Solarladen', true);
|
||||
) 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=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);
|
||||
|
||||
$test->assertSame(
|
||||
1,
|
||||
GetValue($test->objectByIdent('Phasenzahl', $stationId))
|
||||
);
|
||||
$test->assertSame(
|
||||
false,
|
||||
GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))
|
||||
@@ -294,11 +464,11 @@ return static function (TestContext $test): void {
|
||||
GetValue($test->objectByIdent('SollwertGueltig', $stationId))
|
||||
);
|
||||
$test->assertSame(
|
||||
3850,
|
||||
3680,
|
||||
GetValue($test->objectByIdent('Sollleistung', $stationId))
|
||||
);
|
||||
$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))
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user