fix(ladestation): Regeluebergaenge stabilisieren
Tests / test (push) Successful in 50s

This commit is contained in:
dh
2026-10-01 13:01:32 +00:00
parent 8357e8dfab
commit 3ce9af4244
11 changed files with 427 additions and 119 deletions
+2
View File
@@ -15,6 +15,8 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
### Beta-Aenderungen
- Ladestationen halten beim Aktivieren und Umschalten von Solarladen den bisherigen Strom bis zur neuen Manager-Vorgabe; die Stand-alone-Phasenprobe verwendet nur noch 6 A.
- Fahrzeugstatus ist bei beiden Ladestationsvarianten eine Diagnosevariable, Ist- und Sollleistung gehoeren zu den optionalen Visualisierungseinstellungen.
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
- Stand-alone-Ladestationen erkennen nach einer einstellbaren Beobachtungszeit den niedrigeren Fahrzeug-Maximalstrom und begrenzen ihr Leistungsangebot ohne Ladeunterbruch.
+119 -59
View File
@@ -28,17 +28,17 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private const STATUS_GATEWAY_GETRENNT = 202;
private const STATUS_WARTET_AUF_DATEN = 203;
private const STATUS_LADEFEHLER = 204;
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
/** @var list<string> */
private const DIAGNOSE_VARIABLEN = [
'Istleistung',
'Leistungsquelle',
'Sollleistung',
'SollwertGueltig',
'Verfuegbar',
'AenderungMoeglich',
'Stoerung',
'Stoertext',
'Fahrzeugstatus',
'GatewayVerbunden',
'ApiMaximalstrom',
'EaseeAktiviert',
@@ -75,7 +75,6 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'~Switch',
110
);
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
@@ -113,6 +112,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->RegisterAttributeString('LetzterGeraetebefehl', '');
$this->RegisterAttributeString('LetzterStartbefehl', '');
$this->RegisterAttributeBoolean('RegeluebergangAktiv', false);
$this->RegisterTimer(
'Meldezyklus',
@@ -129,6 +129,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
);
$this->RegisterTimer(
'RegeluebergangTimeout',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
);
$this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID);
}
@@ -142,6 +147,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->beendeRegeluebergang();
try {
$this->pruefeKonfiguration();
@@ -167,17 +173,27 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
{
switch ($ident) {
case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) {
$aktiv = (bool) $wert;
$this->SetValue('Aktiv', $aktiv);
if ($aktiv) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false);
}
$this->aktualisiere(true);
return;
case 'Ladefreigabe':
$this->WriteAttributeBoolean('ZustandLadefreigabe', (bool) $wert);
$this->setzeSichtbareVariable('Ladefreigabe', (bool) $wert);
$this->setzeZustand('SollwertGueltig', false);
$ladefreigabe = (bool) $wert;
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
$this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
if ($ladefreigabe) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false);
}
$this->aktualisiere(true);
return;
@@ -185,9 +201,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$solarladen = EaseeLadestatus::solarladenErzwungen(
$this->ReadPropertyInteger('Betriebsmodus')
) ? true : (bool) $wert;
if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
$this->starteRegeluebergang();
}
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
$this->setzeSichtbareVariable('Solarladen', $solarladen);
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(true);
return;
@@ -201,6 +219,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->aktualisiere(true);
return;
case 'RegeluebergangTimeout':
$this->beendeRegeluebergang();
$this->aktualisiere(true);
return;
case 'Melden':
if ((bool) $wert) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
@@ -288,7 +311,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
}
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
$this->aktualisiere(false);
$this->aktualisiere(false, false);
$sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) {
if (
@@ -311,6 +334,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
);
}
$this->beendeRegeluebergang();
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
$this->setzeZustand('Sollleistung', $sollleistung);
@@ -405,7 +429,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeInteger('LetzteBeobachtung', $zeitpunkt);
}
private function aktualisiere(bool $meldungPlanen): void
private function aktualisiere(
bool $meldungPlanen,
bool $steuerungAusfuehren = true
): void
{
try {
$status = EaseeLadestatus::ausBeobachtungen(
@@ -493,12 +520,21 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
}
$this->setzeStoerung($stoertext);
$regeluebergangAktiv = (bool) $this->GetValue('Aktiv')
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
&& $steuerbar
&& $status['FahrzeugVerbunden']
&& !$status['FahrzeugGeladen']
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
if ($gatewayVerbunden && $status['StatusGueltig']) {
$this->setzeLadestrom(
$this->bestimmeWirksameSollleistung($angebot),
$regelPhasenzahl,
$status['Fahrzeugstatus'] === 2
);
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
if ($steuerungAusfuehren && !$regeluebergangAktiv) {
$this->setzeLadestrom(
$sollleistung,
$regelPhasenzahl,
$status['Fahrzeugstatus'] === 2
);
}
} else {
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
}
@@ -797,6 +833,23 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
return is_array($zuordnung) ? $zuordnung : [];
}
private function starteRegeluebergang(): void
{
if ($this->zugeordneteManagerIDs() === []) {
$this->beendeRegeluebergang();
return;
}
$this->WriteAttributeBoolean('RegeluebergangAktiv', true);
$this->SetTimerInterval('RegeluebergangTimeout', self::REGELUEBERGANG_TIMEOUT_MS);
}
private function beendeRegeluebergang(): void
{
$this->WriteAttributeBoolean('RegeluebergangAktiv', false);
$this->SetTimerInterval('RegeluebergangTimeout', 0);
}
private function pruefeKonfiguration(): void
{
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
@@ -857,95 +910,63 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private function aktualisiereVariablen(): void
{
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
$this->RegisterVariableBoolean(
'Ladefreigabe',
'Ladefreigabe',
'~Switch',
150
);
$this->RegisterVariableBoolean(
'Solarladen',
'Solarladen',
'~Switch',
160
);
$this->EnableAction('Ladefreigabe');
$this->EnableAction('Solarladen');
$this->SetValue(
'Ladefreigabe',
$this->ReadAttributeBoolean('ZustandLadefreigabe')
);
$this->SetValue(
'Solarladen',
$this->ReadAttributeBoolean('ZustandSolarladen')
);
} else {
$this->entferneVariable('Ladefreigabe');
$this->entferneVariable('Solarladen');
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose();
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
$this->RegisterVariableBoolean(
'GatewayVerbunden',
'Easee Gateway verbunden',
'~Switch',
200
);
$this->RegisterVariableInteger(
'ApiMaximalstrom',
'API-Maximalstrom',
'',
210
);
$this->RegisterVariableInteger('ApiMaximalstrom', 'API-Maximalstrom', '', 220);
$this->RegisterVariableBoolean(
'EaseeAktiviert',
'Easee aktiviert',
'~Switch',
220
230
);
$this->RegisterVariableBoolean(
'AutorisierungErforderlich',
'Autorisierung erforderlich',
'~Switch',
230
240
);
$this->RegisterVariableBoolean(
'RemoteStartErforderlich',
'Remote-Start erforderlich',
'~Switch',
240
250
);
$this->RegisterVariableBoolean(
'SmartCharging',
'Easee Smart Charging',
'~Switch',
250
260
);
$this->RegisterVariableInteger(
'GrundKeinLadestrom',
'Grund kein Ladestrom',
'',
260
270
);
$this->RegisterVariableString(
'LetzterGeraetebefehl',
'Letzter Geraetebefehl',
'',
270
280
);
$this->RegisterVariableString(
'LetzterStartbefehl',
'Letzter Startbefehl',
'',
280
290
);
$this->RegisterVariableString(
'LeistungsangebotDiagnose',
'Leistungsangebot',
'',
290
300
);
$this->SetValue(
'LeistungsangebotDiagnose',
@@ -983,6 +1004,40 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->entferneVariable($ident);
}
}
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
$this->RegisterVariableBoolean(
'Ladefreigabe',
'Ladefreigabe',
'~Switch',
150
);
$this->RegisterVariableBoolean(
'Solarladen',
'Solarladen',
'~Switch',
160
);
$this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 170);
$this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180);
$this->EnableAction('Ladefreigabe');
$this->EnableAction('Solarladen');
$this->SetValue(
'Ladefreigabe',
$this->ReadAttributeBoolean('ZustandLadefreigabe')
);
$this->SetValue(
'Solarladen',
$this->ReadAttributeBoolean('ZustandSolarladen')
);
$this->SetValue('Istleistung', $this->ReadAttributeFloat('ZustandIstleistung'));
$this->SetValue('Sollleistung', $this->ReadAttributeInteger('ZustandSollleistung'));
} else {
$this->entferneVariable('Ladefreigabe');
$this->entferneVariable('Solarladen');
$this->entferneVariable('Istleistung');
$this->entferneVariable('Sollleistung');
}
}
private function entferneVariable(string $ident): void
@@ -1108,7 +1163,12 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
private function deaktiviereTimer(): void
{
foreach (['Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
foreach ([
'Meldezyklus',
'RueckmeldungVerzoegert',
'VorgabeTimeout',
'RegeluebergangTimeout',
] as $timer) {
$this->SetTimerInterval($timer, 0);
}
}
+105 -35
View File
@@ -23,17 +23,18 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private const STATUS_AKTIV = 102;
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
private const STATUS_KOMMUNIKATIONSFEHLER = 202;
private const REGELUEBERGANG_TIMEOUT_MS = 2000;
private const PHASENERKENNUNGSSTROM = 6;
/** @var list<string> */
private const DIAGNOSE_VARIABLEN = [
'Istleistung',
'Leistungsquelle',
'Sollleistung',
'SollwertGueltig',
'Verfuegbar',
'AenderungMoeglich',
'Stoerung',
'Stoertext',
'Fahrzeugstatus',
'FahrzeugMaximalstrom',
'LetzterGeraetebefehl',
'LeistungsangebotDiagnose',
@@ -67,7 +68,6 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterVariableBoolean('FahrzeugVerbunden', 'Fahrzeug verbunden', '~Switch', 100);
$this->RegisterVariableBoolean('FahrzeugGeladen', 'Fahrzeug geladen', '~Switch', 110);
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
@@ -103,6 +103,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->RegisterAttributeInteger('ErkannterMaximalstrom', 32);
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
$this->RegisterAttributeInteger('FahrzeugstromKandidatSeit', 0);
$this->RegisterAttributeBoolean('RegeluebergangAktiv', false);
$this->RegisterTimer(
'Statuszyklus',
@@ -124,6 +125,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
);
$this->RegisterTimer(
'RegeluebergangTimeout',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'RegeluebergangTimeout', false);"
);
}
public function ApplyChanges(): void
@@ -154,6 +160,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->ReadPropertyInteger('MaximalerLadestrom')
);
$this->setzeFahrzeugstromKandidatZurueck();
$this->beendeRegeluebergang();
try {
$this->pruefeKonfiguration();
@@ -176,18 +183,37 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
{
switch ($ident) {
case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) {
$aktiv = (bool) $wert;
$this->SetValue('Aktiv', $aktiv);
if ($aktiv) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false);
}
$this->aktualisiere(true);
return;
case 'Ladefreigabe':
$ladefreigabe = (bool) $wert;
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
$this->setzeSichtbareVariable('Ladefreigabe', $ladefreigabe);
if ($ladefreigabe) {
$this->starteRegeluebergang();
} else {
$this->beendeRegeluebergang();
$this->setzeZustand('SollwertGueltig', false);
}
$this->aktualisiere(true);
return;
case 'Solarladen':
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert);
$this->setzeSichtbareVariable($ident, (bool) $wert);
$this->setzeZustand('SollwertGueltig', false);
$solarladen = (bool) $wert;
if ($solarladen !== $this->ReadAttributeBoolean('ZustandSolarladen')) {
$this->starteRegeluebergang();
}
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
$this->setzeSichtbareVariable('Solarladen', $solarladen);
$this->aktualisiere(true);
return;
@@ -201,6 +227,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->aktualisiere(true);
return;
case 'RegeluebergangTimeout':
$this->beendeRegeluebergang();
$this->aktualisiere(true);
return;
case 'Melden':
if ((bool) $wert) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
@@ -238,7 +269,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
}
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
$this->aktualisiere(false, false);
$this->aktualisiere(false, false, false);
$sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) {
if ((bool) $this->leseZustand('SollwertGueltig')
@@ -256,6 +287,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
);
}
$this->beendeRegeluebergang();
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
$this->setzeZustand('Sollleistung', $sollleistung);
@@ -265,7 +297,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
}
private function aktualisiere(bool $meldungPlanen, bool $statusLesen = true): void
private function aktualisiere(
bool $meldungPlanen,
bool $statusLesen = true,
bool $steuerungAusfuehren = true
): void
{
try {
if ($statusLesen) {
@@ -346,12 +382,17 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
if (!($aktiv
$phasenerkennungAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
)) {
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT;
$regeluebergangAktiv = $aktiv
&& $ladefreigabe
&& $verbunden
&& !$geladen
&& $this->ReadAttributeBoolean('RegeluebergangAktiv');
if ($steuerungAusfuehren && !$phasenerkennungAktiv && !$regeluebergangAktiv) {
$this->setzeLadestrom($sollleistung);
}
$this->setzeStoerung('');
@@ -413,17 +454,16 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
) {
$erkennungSeit = $this->ReadAttributeInteger('PhasenerkennungSeit');
if ($erkennungSeit <= 0) {
$this->sendeLadestromAnGeraet(
$this->ReadPropertyInteger('MaximalerLadestrom')
);
$this->sendeLadestromAnGeraet(self::PHASENERKENNUNGSSTROM);
$erkennungSeit = time();
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
} elseif ((time() - $erkennungSeit)
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
) {
$bekanntePhasenzahl = $rohstatus['Istleistung_W'] > 7500.0
? LadestationRegler::PHASEN_DREI
: LadestationRegler::PHASEN_EIN;
$bekanntePhasenzahl = LadestationRegler::erkennePhasenzahlAusLeistung(
(float) $rohstatus['Istleistung_W'],
$this->ReadAttributeInteger('LetzterGesetzterStrom')
);
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
}
} else {
@@ -819,6 +859,23 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
return is_array($zuordnung) ? $zuordnung : [];
}
private function starteRegeluebergang(): void
{
if ($this->zugeordneteManagerIDs() === []) {
$this->beendeRegeluebergang();
return;
}
$this->WriteAttributeBoolean('RegeluebergangAktiv', true);
$this->SetTimerInterval('RegeluebergangTimeout', self::REGELUEBERGANG_TIMEOUT_MS);
}
private function beendeRegeluebergang(): void
{
$this->WriteAttributeBoolean('RegeluebergangAktiv', false);
$this->SetTimerInterval('RegeluebergangTimeout', 0);
}
private function pruefeKonfiguration(): void
{
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
@@ -882,27 +939,16 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private function aktualisiereVariablen(): void
{
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
$this->RegisterVariableBoolean('Ladefreigabe', 'Ladefreigabe', '~Switch', 150);
$this->RegisterVariableBoolean('Solarladen', 'Solarladen', '~Switch', 160);
$this->EnableAction('Ladefreigabe');
$this->EnableAction('Solarladen');
$this->SetValue('Ladefreigabe', $this->ReadAttributeBoolean('ZustandLadefreigabe'));
$this->SetValue('Solarladen', $this->ReadAttributeBoolean('ZustandSolarladen'));
} else {
$this->entferneVariable('Ladefreigabe');
$this->entferneVariable('Solarladen');
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose();
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200);
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210);
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200);
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 210);
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 220);
$this->RegisterVariableInteger(
'FahrzeugMaximalstrom',
'Erkannter Fahrzeug-Maximalstrom',
'',
220
230
);
$this->SetValue(
'FahrzeugMaximalstrom',
@@ -926,6 +972,24 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->entferneVariable($ident);
}
}
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
$this->RegisterVariableBoolean('Ladefreigabe', 'Ladefreigabe', '~Switch', 150);
$this->RegisterVariableBoolean('Solarladen', 'Solarladen', '~Switch', 160);
$this->RegisterVariableFloat('Istleistung', 'Istleistung', '', 170);
$this->RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180);
$this->EnableAction('Ladefreigabe');
$this->EnableAction('Solarladen');
$this->SetValue('Ladefreigabe', $this->ReadAttributeBoolean('ZustandLadefreigabe'));
$this->SetValue('Solarladen', $this->ReadAttributeBoolean('ZustandSolarladen'));
$this->SetValue('Istleistung', $this->ReadAttributeFloat('ZustandIstleistung'));
$this->SetValue('Sollleistung', $this->ReadAttributeInteger('ZustandSollleistung'));
} else {
$this->entferneVariable('Ladefreigabe');
$this->entferneVariable('Solarladen');
$this->entferneVariable('Istleistung');
$this->entferneVariable('Sollleistung');
}
}
private function entferneVariable(string $ident): void
@@ -1033,7 +1097,13 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
private function deaktiviereTimer(): void
{
foreach (['Statuszyklus', 'Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
foreach ([
'Statuszyklus',
'Meldezyklus',
'RueckmeldungVerzoegert',
'VorgabeTimeout',
'RegeluebergangTimeout',
] as $timer) {
$this->SetTimerInterval($timer, 0);
}
}
+11 -6
View File
@@ -50,22 +50,22 @@ als ungueltig behandelt und ergeben bis zur gueltigen 1-/3-Phasenmeldung nur
| `PrioritaetPeak` | Integer | `0` | Prioritaet im Peakbetrieb. |
| `Meldeintervall` | Integer | `10 s` | Periodische Vollmeldung an den Manager. |
| `VorgabeTimeout` | Integer | `120 s` | Ablauf einer nicht erneuerten Manager-Vorgabe. |
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Ladefreigabe und Solarladen. |
| `EinstellungenInVisu` | Boolean | `false` | Zeigt Ladefreigabe, Solarladen, Istleistung und Sollleistung. |
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Zeigt technische Diagnosewerte. |
| `LoggingEin` | Boolean | `false` | Aktiviert Debugmeldungen ohne Geheimnisse. |
## Variablen
Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`,
`Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` wartet auf Start,
`3` laedt, `4` geladen oder `5` Fehler.
`Ladestrom` und `Phasenzahl`. Mit `EinstellungenInVisu=true` erscheinen
zusaetzlich `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung`.
Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie
Mit `DiagnosevariablenAnzeigen=true` werden der numerische `Fahrzeugstatus`
und die gemeinsamen EMS-Diagnosewerte sowie
`GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
`AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
`LeistungsangebotDiagnose`. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
`LeistungsangebotDiagnose` angezeigt. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
die Autorisierung fehlt.
## Regelung
@@ -83,6 +83,11 @@ Ladestation Stand-Alone:
Die niedrigste Grenze aus Property, Observation `47` und Observation `104`
bestimmt den angebotenen Maximalstrom. Eine neue Observation berechnet das
Angebot sofort neu und meldet die Aenderung kurz gebuendelt an den Manager.
Beim Aktivieren oder Umschalten von Solarladen bleibt der bisherige
Geraetestrom bis zur neuen Manager-Vorgabe unveraendert. Ohne Managerantwort
greift nach zwei Sekunden das lokale Ersatzverhalten. Sicherheitsabschaltungen
werden nicht verzoegert.
Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0`
+20 -14
View File
@@ -43,11 +43,11 @@ Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
Strom fuehren, wird dreiphasiges Laden direkt erkannt.
4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
Modul zunaechst den konfigurierten Maximalstrom als Erkennungsstrom vor. Bis
zur abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot
`[0]`.
5. Nach `Phasenerkennungszeit` gilt wie in Enelix 1: ueber 7500 W werden drei
Phasen erkannt, andernfalls eine Phase. Die erkannte Phasenzahl bleibt bis
Modul zunaechst nur den Mindeststrom von 6 A als Erkennungsstrom vor. Bis zur
abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot `[0]`.
5. Nach `Phasenerkennungszeit` wird die gemessene Leistung relativ zum
angeforderten Erkennungsstrom ausgewertet. Dadurch lassen sich eine und drei
Phasen bereits bei 6 A unterscheiden. Die erkannte Phasenzahl bleibt bis
zum Ausstecken erhalten und kippt waehrend einer Ladepause nicht zurueck.
6. Der Ladestrom wird aus der Leistung mit `230 W/A` einphasig beziehungsweise
`684 W/A` dreiphasig berechnet.
@@ -84,6 +84,12 @@ Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
die maximale angebotene Leistung.
Beim Aktivieren oder Umschalten von Solarladen bleibt ein bereits gesetzter
Geraetestrom zunaechst unveraendert. Das neue Angebot wird zuerst an den
zugeordneten Manager gemeldet; dessen neue Vorgabe wird anschliessend direkt
uebernommen. Antwortet kein Manager, greift nach zwei Sekunden das lokale
Ersatzverhalten. Sicherheitsabschaltungen wirken weiterhin sofort.
Nach einer Leistungsaenderung wird die aktuelle Stufe fuer
`ZeitZwischenZustandswechseln` gehalten. Waehrend der
`Mindesteinschaltdauer` bleiben positive Leistungsstufen regelbar, `0 W`
@@ -115,7 +121,7 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
| `PrioritaetPeak` | Integer | `0` | Prioritaet des Verbrauchers im Peakbetrieb. |
| `Meldeintervall` | Integer | `10 s` | Intervall fuer die periodische Vollmeldung an zugeordnete Manager. |
| `VorgabeTimeout` | Integer | `120 s` | Gueltigkeitsdauer einer Manager-Vorgabe ohne Erneuerung. |
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe` und `Solarladen` als bedienbare Variablen ein. |
| `EinstellungenInVisu` | Boolean | `false` | Blendet `Ladefreigabe`, `Solarladen`, `Istleistung` und `Sollleistung` ein. |
| `LoggingEin` | Boolean | `false` | Aktiviert das laufende Debug-Protokoll des Moduls. |
### Ladestation und Diagnose
@@ -129,7 +135,7 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der Maximalstrom-Probe vor der leistungsbasierten Phasenerkennung. |
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der 6-A-Probe vor der leistungsbasierten Phasenerkennung. |
| `FahrzeugstromErkennungszeit` | Integer | `90 s` | Zeit, die eine stabile Stromunterschreitung anliegen muss, bevor die Fahrzeuggrenze mit 2,5 A Reserve uebernommen wird. |
| `ZeitZwischenZustandswechseln` | Integer | `1 min` | Mindestabstand zwischen zwei Leistungsaenderungen. |
| `Mindesteinschaltdauer` | Integer | `0 min` | Mindestdauer nach dem Einschalten, in der kein Abschalten auf 0 W angeboten wird. |
@@ -152,22 +158,22 @@ Immer sichtbar:
| `Aktiv` | Boolean / bedienbar | Lokale EMS-Freigabe; der Initialwert ist `false`. |
| `FahrzeugVerbunden` | Boolean / Anzeige | Zeigt, ob die API ein verbundenes Fahrzeug meldet. |
| `FahrzeugGeladen` | Boolean / Anzeige | Ergebnis der kompatiblen Enelix-1-Voll-Erkennung. |
| `Fahrzeugstatus` | Integer / Anzeige | Normalisierter Status von `0` bis `4`. |
| `Ladestrom` | Float / Anzeige | Aus der gemessenen Leistung ermittelter Ladestrom in A. |
| `Phasenzahl` | Integer / Anzeige | `0` unbekannt, `1` einphasig oder `3` dreiphasig. |
Nur mit `EinstellungenInVisu=true` sichtbar und bedienbar:
Nur mit `EinstellungenInVisu=true` sichtbar; Ladefreigabe und Solarladen sind bedienbar:
- `Ladefreigabe`
- `Solarladen`
- `Istleistung`
- `Sollleistung`
Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
| Ident | Beschreibung |
| --- | --- |
| `Istleistung` | Von der Ladestation gemessene Leistung in W. |
| `Leistungsquelle` | Immer `2` fuer eine gemessene Leistung. |
| `Sollleistung` | Letzte angenommene Manager-Vorgabe in W. |
| `Fahrzeugstatus` | Normalisierter Status von `0` bis `4`. |
| `SollwertGueltig` | Zeigt, ob die Vorgabe noch gueltig und im aktuellen Angebot enthalten ist. |
| `Verfuegbar` | Zeigt, ob die Ladestation grundsaetzlich Leistung aufnehmen kann. |
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
@@ -253,8 +259,8 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
Leistungsangebot `[0]`.
- Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
zunaechst der konfigurierte Maximalstrom zur Erkennung angefordert.
- Nach der Erkennungszeit werden bis 7500 W eine Phase, darueber drei Phasen
zunaechst der Mindeststrom von 6 A zur Erkennung angefordert.
- Nach der Erkennungszeit werden eine oder drei Phasen relativ zum Pruefstrom
gespeichert. Bei go-e koennen die einzelnen Phasenstroeme die Erkennung
bereits vorher abschliessen.
- Die erkannte Phasenzahl bleibt bei einer anschliessenden Ladepause stabil.
@@ -298,7 +304,7 @@ URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
geprueft.
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
API-Antworten. Er prueft alle drei Geraetevarianten, die Maximalstrom-Probe
API-Antworten. Er prueft alle drei Geraetevarianten, die Mindeststrom-Probe
beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
Umschaltung ueber den `Solarladen`-Button.
+16
View File
@@ -15,6 +15,22 @@ final class LadestationRegler
private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
public static function erkennePhasenzahlAusLeistung(
float $leistung,
int $angeforderterStrom
): int {
if (!is_finite($leistung) || $leistung < 0.0) {
throw new InvalidArgumentException('Istleistung muss endlich und darf nicht negativ sein.');
}
if ($angeforderterStrom < 6 || $angeforderterStrom > 32) {
throw new InvalidArgumentException('Erkennungsstrom muss zwischen 6 und 32 A liegen.');
}
$trennleistung = 2 * self::LEISTUNG_PRO_AMPERE_EINPHASIG * $angeforderterStrom;
return $leistung > $trennleistung ? self::PHASEN_DREI : self::PHASEN_EIN;
}
/**
* Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
* Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
+13
View File
@@ -70,6 +70,19 @@ final class EaseeModuleStrukturTest extends TestCase
self::assertStringContainsString("'Phasenzahl'", $modul);
self::assertStringNotContainsString('7500', $modul);
self::assertStringNotContainsString('Abfrageintervall', $modul);
self::assertStringContainsString(
"RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200)",
$modul
);
self::assertStringContainsString(
"RegisterVariableFloat('Istleistung', 'Istleistung', '', 170)",
$modul
);
self::assertStringContainsString(
"RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180)",
$modul
);
self::assertStringContainsString('RegeluebergangTimeout', $modul);
self::assertStringContainsString("'action' => 'StartCharging'", $modul);
$gateway = (string) file_get_contents(
+18
View File
@@ -9,6 +9,24 @@ use PHPUnit\Framework\TestCase;
final class LadestationReglerTest extends TestCase
{
public function testPhasenprobeUnterscheidetBeiMindeststromOhneMaximalstromsprung(): void
{
self::assertSame(
LadestationRegler::PHASEN_EIN,
LadestationRegler::erkennePhasenzahlAusLeistung(1380.0, 6)
);
self::assertSame(
LadestationRegler::PHASEN_DREI,
LadestationRegler::erkennePhasenzahlAusLeistung(4104.0, 6)
);
}
public function testPhasenprobeWeistUngueltigenErkennungsstromAb(): void
{
$this->expectException(\InvalidArgumentException::class);
LadestationRegler::erkennePhasenzahlAusLeistung(4104.0, 5);
}
public function testFahrzeugUndDreiPhasenWerdenWieInEnelixEinsErkannt(): void
{
$status = LadestationRegler::normalisiereStatus([
@@ -88,6 +88,20 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
self::assertStringContainsString("ReadPropertyBoolean('EinstellungenInVisu')", $modul);
self::assertStringContainsString(
"RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 200)",
$modul
);
self::assertStringContainsString(
"RegisterVariableFloat('Istleistung', 'Istleistung', '', 170)",
$modul
);
self::assertStringContainsString(
"RegisterVariableInteger('Sollleistung', 'Sollleistung', '', 180)",
$modul
);
self::assertStringContainsString('RegeluebergangTimeout', $modul);
self::assertStringContainsString('PHASENERKENNUNGSSTROM = 6', $modul);
self::assertStringContainsString('UnregisterVariable($ident)', $modul);
}
}
@@ -184,6 +184,58 @@ return static function (TestContext $test): void {
$test->assertInstanceStatus($station);
});
$test->runCase('Solarumschaltung wartet auf die neue Manager-Vorgabe', static function (
TestContext $test
) use ($station): void {
$managerId = $test->createInstance(
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
'Hilfsmanager Easee-Uebergang',
[
'AutomatischeSuche' => false,
'VerbraucherZuordnung' => json_encode([
['InstanzID' => $station, 'Aktiv' => true],
], JSON_THROW_ON_ERROR),
]
);
$sendeSollwert = static function (int $leistung) use ($managerId, $station): void {
IPS_RequestAction($station, 'ManagerdatenEmpfangen', json_encode([
'Kopf' => [
'Version' => '4.0',
'AbsenderID' => $managerId,
'EmpfaengerID' => $station,
'Zeitpunkt' => time(),
],
'Betriebsart' => 'PV',
'Sollleistung_W' => $leistung,
], JSON_THROW_ON_ERROR));
};
$sendeSollwert(4104);
$befehlVorUmschaltung = (string) GetValue(
$test->objectByIdent('LetzterGeraetebefehl', $station)
);
IPS_RequestAction($station, 'Solarladen', false);
$test->assertSame(
$befehlVorUmschaltung,
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station))
);
$test->assertSame(
4104,
GetValue($test->objectByIdent('Sollleistung', $station)),
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.'
);
$sendeSollwert(10944);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $station)),
'"Ampere":16'
));
IPS_RequestAction($station, 'Solarladen', true);
$sendeSollwert(0);
});
$test->runCase('SignalR-Abbruch erreicht die Ladestation', static function (
TestContext $test
) use ($gateway, $station): void {
@@ -226,4 +278,24 @@ return static function (TestContext $test): void {
);
$test->assertInstanceStatus($station, 202);
});
$test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function (
TestContext $test
) use ($station): void {
IPS_SetProperty($station, 'EinstellungenInVisu', true);
IPS_SetProperty($station, 'DiagnosevariablenAnzeigen', false);
IPS_ApplyChanges($station);
$test->assertTrue(@IPS_GetObjectIDByIdent('Istleistung', $station) !== false);
$test->assertTrue(@IPS_GetObjectIDByIdent('Sollleistung', $station) !== false);
$test->assertSame(false, @IPS_GetObjectIDByIdent('Fahrzeugstatus', $station));
IPS_SetProperty($station, 'EinstellungenInVisu', false);
IPS_SetProperty($station, 'DiagnosevariablenAnzeigen', true);
IPS_ApplyChanges($station);
$test->assertSame(false, @IPS_GetObjectIDByIdent('Istleistung', $station));
$test->assertSame(false, @IPS_GetObjectIDByIdent('Sollleistung', $station));
$test->assertTrue(@IPS_GetObjectIDByIdent('Fahrzeugstatus', $station) !== false);
});
};
+37 -5
View File
@@ -361,7 +361,18 @@ return static function (TestContext $test): void {
'amp=6'
), 'Solarladen: Der 6-A-Befehl fehlt. Erhalten: ' . $befehl . '; Diagnose: ' . $diagnose);
$befehlVorUmschaltung = $befehl;
IPS_RequestAction($stationId, 'Solarladen', false);
$test->assertSame(
$befehlVorUmschaltung,
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'Die Umschaltung darf vor der neuen Manager-Vorgabe keinen Zwischenwert setzen.'
);
$test->assertSame(
4104,
GetValue($test->objectByIdent('Sollleistung', $stationId)),
'Die Sollleistung muss bis zur neuen Manager-Vorgabe stabil bleiben.'
);
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
$test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
$abgewiesen = false;
@@ -422,7 +433,7 @@ return static function (TestContext $test): void {
));
});
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Maximalstrom erkannt', static function (
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Mindeststrom erkannt', static function (
TestContext $test
) use ($phasenerkennung, $anforderungSenden): void {
$stationId = $phasenerkennung;
@@ -432,8 +443,8 @@ return static function (TestContext $test): void {
);
$test->assertTrue(!str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=16'
), 'Eine inaktive Station darf keine Maximalstrom-Probe starten.');
'amp=6'
), 'Eine inaktive Station darf keine Phasenprobe starten.');
IPS_RequestAction($stationId, 'Aktiv', true);
@@ -443,8 +454,8 @@ return static function (TestContext $test): void {
);
$test->assertTrue(str_contains(
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
'amp=16'
), 'Nach der Freigabe muss die Maximalstrom-Probe starten.');
'amp=6'
), 'Nach der Freigabe muss die Mindeststrom-Phasenprobe starten.');
IPS_Sleep(2100);
IPS_RequestAction($stationId, 'Statuszyklus', false);
@@ -472,4 +483,25 @@ return static function (TestContext $test): void {
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
);
});
$test->runCase('Leistungswerte und Fahrzeugstatus folgen ihrer Sichtbarkeitsgruppe', static function (
TestContext $test
) use ($stationen): void {
$stationId = $stationen['go-e alt'];
IPS_SetProperty($stationId, 'EinstellungenInVisu', true);
IPS_SetProperty($stationId, 'DiagnosevariablenAnzeigen', false);
IPS_ApplyChanges($stationId);
$test->assertTrue(@IPS_GetObjectIDByIdent('Istleistung', $stationId) !== false);
$test->assertTrue(@IPS_GetObjectIDByIdent('Sollleistung', $stationId) !== false);
$test->assertSame(false, @IPS_GetObjectIDByIdent('Fahrzeugstatus', $stationId));
IPS_SetProperty($stationId, 'EinstellungenInVisu', false);
IPS_SetProperty($stationId, 'DiagnosevariablenAnzeigen', true);
IPS_ApplyChanges($stationId);
$test->assertSame(false, @IPS_GetObjectIDByIdent('Istleistung', $stationId));
$test->assertSame(false, @IPS_GetObjectIDByIdent('Sollleistung', $stationId));
$test->assertTrue(@IPS_GetObjectIDByIdent('Fahrzeugstatus', $stationId) !== false);
});
};