diff --git a/LadestationStandAlone/form.json b/LadestationStandAlone/form.json index 2225c57..5929e6d 100644 --- a/LadestationStandAlone/form.json +++ b/LadestationStandAlone/form.json @@ -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 } ] }, diff --git a/LadestationStandAlone/module.php b/LadestationStandAlone/module.php index 036b148..2f87ebe 100644 --- a/LadestationStandAlone/module.php +++ b/LadestationStandAlone/module.php @@ -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.'); diff --git a/docs/module/Ladestation-Stand-Alone/README.md b/docs/module/Ladestation-Stand-Alone/README.md index 718e441..f10ec75 100644 --- a/docs/module/Ladestation-Stand-Alone/README.md +++ b/docs/module/Ladestation-Stand-Alone/README.md @@ -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: diff --git a/libs/LadestationAdapter.php b/libs/LadestationAdapter.php index d280381..322cfaf 100644 --- a/libs/LadestationAdapter.php +++ b/libs/LadestationAdapter.php @@ -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; diff --git a/libs/LadestationRegler.php b/libs/LadestationRegler.php index 5eddd13..bfbc176 100644 --- a/libs/LadestationRegler.php +++ b/libs/LadestationRegler.php @@ -12,27 +12,15 @@ final class LadestationRegler public const PHASEN_EIN = 1; public const PHASEN_DREI = 3; - /** @var list */ - 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 */ - 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 */ 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) + ); } } diff --git a/tests/LadestationAdapterTest.php b/tests/LadestationAdapterTest.php index 022c836..194b09a 100644 --- a/tests/LadestationAdapterTest.php +++ b/tests/LadestationAdapterTest.php @@ -13,7 +13,7 @@ final class LadestationAdapterTest extends TestCase /** * @dataProvider statusFaelle * @param array $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, diff --git a/tests/LadestationReglerTest.php b/tests/LadestationReglerTest.php index e8eb3e4..043313a 100644 --- a/tests/LadestationReglerTest.php +++ b/tests/LadestationReglerTest.php @@ -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)); } } diff --git a/tests/LadestationStandAloneModulstrukturTest.php b/tests/LadestationStandAloneModulstrukturTest.php index c015b79..3b22a14 100644 --- a/tests/LadestationStandAloneModulstrukturTest.php +++ b/tests/LadestationStandAloneModulstrukturTest.php @@ -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); diff --git a/tests/Symcon/modules/LadestationGateway.php b/tests/Symcon/modules/LadestationGateway.php index b548786..d1716f6 100644 --- a/tests/Symcon/modules/LadestationGateway.php +++ b/tests/Symcon/modules/LadestationGateway.php @@ -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) ) diff --git a/tests/Symcon/modules/LadestationStandAlone.php b/tests/Symcon/modules/LadestationStandAlone.php index a1c122b..9e9b3df 100644 --- a/tests/Symcon/modules/LadestationStandAlone.php +++ b/tests/Symcon/modules/LadestationStandAlone.php @@ -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)) ); });