Author SHA1 Message Date
dh ccecc437e9 fix(easee): wartende Ladevorgaenge starten
Tests / test (push) Successful in 45s
2026-09-29 09:34:37 +00:00
dh 156591a91e fix(ladestation): Ladeende nach Update neu prüfen
Tests / test (push) Successful in 47s
2026-09-29 09:19:56 +00:00
dh 3cabf597ac fix(ladestation): Solarpause nicht als Ladeende erkennen 2026-09-29 09:07:56 +00:00
dh d1fc62d43c fix(manager): Regelung bei veralteten Verbrauchern fortsetzen
Tests / test (push) Successful in 46s
2026-09-29 08:46:44 +00:00
dh aefe46352f fix(easee): Wartezustand ohne Ausgangsphase zulassen
Tests / test (push) Successful in 48s
2026-09-29 08:25:08 +00:00
dh 6f76e6cd86 fix(easee): Initialstatus der Ladestation laden
Tests / test (push) Successful in 48s
2026-09-29 07:55:31 +00:00
21 changed files with 627 additions and 66 deletions
+4
View File
@@ -17,6 +17,10 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
- Stufenlose Leistungsbereiche fuer Batteriespeicher. - Stufenlose Leistungsbereiche fuer Batteriespeicher.
- Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert. - Ladefreigabe neuer Ladestationsinstanzen standardmaessig aktiviert.
- Easee-Ladestationen laden einen fehlenden Initialstatus ueber die Observations-API nach und starten bei noch unbekannter Ausgangsphase konservativ einphasig.
- Wartende Easee-Ladestationen erhalten bei positiver Stromvorgabe einmal pro Fahrzeugverbindung einen Startbefehl; Freigabe- und Blockiergruende werden diagnostiziert.
- Fehlende oder veraltete Verbraucher blockieren die Verteilung an aktuelle Verbraucher nicht mehr.
- Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an.
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module. - Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
### Hinweise ### Hinweise
+98 -10
View File
@@ -221,6 +221,12 @@ class EaseeGateway extends IPSModule
$seriennummer, $seriennummer,
(float) ($anfrage['amps'] ?? -1) (float) ($anfrage['amps'] ?? -1)
), JSON_THROW_ON_ERROR); ), JSON_THROW_ON_ERROR);
case 'StartCharging':
return json_encode(
$this->sendeLadestart($seriennummer),
JSON_THROW_ON_ERROR
);
} }
return json_encode([ return json_encode([
@@ -530,6 +536,25 @@ class EaseeGateway extends IPSModule
return; return;
} }
$this->speichereBeobachtungImCache($seriennummer, $id, $wert);
$this->SendDataToChildren(json_encode([
'DataID' => self::CHILD_EVENT_DATA_ID,
'Buffer' => json_encode([
'type' => 'Observation',
'serialNumber' => $seriennummer,
'id' => $id,
'value' => $wert,
'timestamp' => time(),
], JSON_THROW_ON_ERROR),
], JSON_THROW_ON_ERROR));
}
private function speichereBeobachtungImCache(
string $seriennummer,
int $id,
$wert
): void {
$cache = $this->leseAttributArray('ObservationCache'); $cache = $this->leseAttributArray('ObservationCache');
if (!isset($cache[$seriennummer]) || !is_array($cache[$seriennummer])) { if (!isset($cache[$seriennummer]) || !is_array($cache[$seriennummer])) {
$cache[$seriennummer] = []; $cache[$seriennummer] = [];
@@ -545,21 +570,66 @@ class EaseeGateway extends IPSModule
'ObservationCache', 'ObservationCache',
json_encode($cache, JSON_THROW_ON_ERROR) json_encode($cache, JSON_THROW_ON_ERROR)
); );
}
$this->SendDataToChildren(json_encode([ private function stelleAktuellenStatusSicher(string $seriennummer): void
'DataID' => self::CHILD_EVENT_DATA_ID, {
'Buffer' => json_encode([ $cache = $this->leseAttributArray('ObservationCache');
'type' => 'Observation', $zustand = $cache[$seriennummer] ?? [];
'serialNumber' => $seriennummer, if (
'id' => $id, (is_array($zustand) && array_key_exists('109', $zustand))
'value' => $wert, || $this->ReadPropertyBoolean('Testmodus')
'timestamp' => time(), || $this->GetBuffer('SignalRReady') !== '1'
], JSON_THROW_ON_ERROR), ) {
], JSON_THROW_ON_ERROR)); return;
}
$antwort = $this->autorisierteApiAnfrage(
'GET',
'/state/' . rawurlencode($seriennummer) . '/observations?ids='
. rawurlencode(implode(',', EaseeGatewayProtokoll::BEOBACHTUNGEN)),
''
);
if (!($antwort['success'] ?? false)) {
$this->setzeLetztenFehler(
'Easee-Status konnte nicht geladen werden: '
. (string) ($antwort['error'] ?? 'Unbekannter API-Fehler.')
);
return;
}
$daten = json_decode((string) ($antwort['body'] ?? ''), true);
if (!is_array($daten)) {
$this->setzeLetztenFehler('Easee-Statusantwort ist ungueltig.');
return;
}
$beobachtungen = EaseeGatewayProtokoll::extrahiereBeobachtungen(
$daten,
[$seriennummer],
$seriennummer
);
$betriebsstatusGeladen = false;
foreach ($beobachtungen as $beobachtung) {
$this->speichereBeobachtungImCache(
$beobachtung['Seriennummer'],
$beobachtung['ID'],
$beobachtung['Wert']
);
$betriebsstatusGeladen = $betriebsstatusGeladen
|| $beobachtung['ID'] === 109;
}
if (!$betriebsstatusGeladen) {
$this->setzeLetztenFehler(
'Easee-Statusantwort enthaelt keinen Betriebsstatus (109).'
);
return;
}
$this->setzeLetztenFehler('');
} }
private function erstelleStatusantwort(string $seriennummer): string private function erstelleStatusantwort(string $seriennummer): string
{ {
$this->stelleAktuellenStatusSicher($seriennummer);
$cache = $this->leseAttributArray('ObservationCache'); $cache = $this->leseAttributArray('ObservationCache');
return json_encode([ return json_encode([
'success' => true, 'success' => true,
@@ -606,6 +676,24 @@ class EaseeGateway extends IPSModule
); );
} }
/** @return array<string, mixed> */
private function sendeLadestart(string $seriennummer): array
{
if ($seriennummer === '') {
return ['success' => false, 'error' => 'Seriennummer fehlt.'];
}
if ($this->ReadPropertyBoolean('Testmodus')) {
return ['success' => true, 'httpCode' => 200, 'body' => '{}'];
}
return $this->autorisierteApiAnfrage(
'POST',
'/api/chargers/' . rawurlencode($seriennummer)
. '/commands/start_charging',
''
);
}
/** @return array<string, mixed> */ /** @return array<string, mixed> */
private function autorisierteApiAnfrage( private function autorisierteApiAnfrage(
string $methode, string $methode,
+156 -19
View File
@@ -41,7 +41,13 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Stoertext', 'Stoertext',
'GatewayVerbunden', 'GatewayVerbunden',
'ApiMaximalstrom', 'ApiMaximalstrom',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
]; ];
@@ -82,6 +88,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeString('EaseeBeobachtungen', '{}'); $this->RegisterAttributeString('EaseeBeobachtungen', '{}');
$this->RegisterAttributeInteger('LetzteBeobachtung', 0); $this->RegisterAttributeInteger('LetzteBeobachtung', 0);
$this->RegisterAttributeInteger('ApiMaximalstrom', 16); $this->RegisterAttributeInteger('ApiMaximalstrom', 16);
$this->RegisterAttributeBoolean('ZustandEaseeAktiviert', true);
$this->RegisterAttributeBoolean('ZustandAutorisierungErforderlich', false);
$this->RegisterAttributeBoolean('ZustandRemoteStartErforderlich', false);
$this->RegisterAttributeBoolean('ZustandSmartCharging', false);
$this->RegisterAttributeInteger('ZustandGrundKeinLadestrom', 0);
$this->RegisterAttributeBoolean('ZustandFahrzeugVerbunden', false); $this->RegisterAttributeBoolean('ZustandFahrzeugVerbunden', false);
$this->RegisterAttributeBoolean('ZustandFahrzeugGeladen', false); $this->RegisterAttributeBoolean('ZustandFahrzeugGeladen', false);
$this->RegisterAttributeInteger('ZustandFahrzeugstatus', 0); $this->RegisterAttributeInteger('ZustandFahrzeugstatus', 0);
@@ -99,7 +110,9 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeInteger('LetzteManagerID', 0); $this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0); $this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1); $this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->RegisterAttributeString('LetzterGeraetebefehl', ''); $this->RegisterAttributeString('LetzterGeraetebefehl', '');
$this->RegisterAttributeString('LetzterStartbefehl', '');
$this->RegisterTimer( $this->RegisterTimer(
'Meldezyklus', 'Meldezyklus',
@@ -128,6 +141,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
$this->WriteAttributeBoolean('GatewayVerbunden', false); $this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
@@ -405,6 +419,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Phasenzahl', 'Phasenzahl',
'Istleistung_W', 'Istleistung_W',
'Ladestrom_A', 'Ladestrom_A',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
] as $feld) { ] as $feld) {
$this->setzeZustand($feld, $status[$feld]); $this->setzeZustand($feld, $status[$feld]);
} }
@@ -416,13 +435,19 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'ApiMaximalstrom', 'ApiMaximalstrom',
$status['ApiMaximalstrom_A'] $status['ApiMaximalstrom_A']
); );
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
}
$gatewayVerbunden = $this->ReadAttributeBoolean('GatewayVerbunden'); $gatewayVerbunden = $this->ReadAttributeBoolean('GatewayVerbunden');
$phaseGueltig = in_array($status['Phasenzahl'], [1, 3], true); $phaseGueltig = in_array($status['Phasenzahl'], [1, 3], true);
$regelPhasenzahl = $phaseGueltig ? $status['Phasenzahl'] : 1;
$phaseErforderlich = $status['Fahrzeugstatus'] === 3;
$steuerbar = $gatewayVerbunden $steuerbar = $gatewayVerbunden
&& $status['StatusGueltig'] && $status['StatusGueltig']
&& $status['EaseeAktiviert']
&& !$status['Stoerung'] && !$status['Stoerung']
&& (!$status['FahrzeugVerbunden'] || $phaseGueltig); && (!$phaseErforderlich || $phaseGueltig);
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen'); $solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
$peakbetrieb = $this->ReadAttributeString('Betriebsart') $peakbetrieb = $this->ReadAttributeString('Betriebsart')
@@ -432,7 +457,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->ReadAttributeBoolean('ZustandLadefreigabe'), $this->ReadAttributeBoolean('ZustandLadefreigabe'),
$steuerbar && $status['FahrzeugVerbunden'], $steuerbar && $status['FahrzeugVerbunden'],
$status['FahrzeugGeladen'], $status['FahrzeugGeladen'],
$phaseGueltig ? $status['Phasenzahl'] : 1, $regelPhasenzahl,
$status['ApiMaximalstrom_A'], $status['ApiMaximalstrom_A'],
$solarladen, $solarladen,
$peakbetrieb $peakbetrieb
@@ -460,8 +485,8 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
} elseif ($status['Stoerung']) { } elseif ($status['Stoerung']) {
$stoertext = $status['Stoertext']; $stoertext = $status['Stoertext'];
$this->SetStatus(self::STATUS_LADEFEHLER); $this->SetStatus(self::STATUS_LADEFEHLER);
} elseif ($status['FahrzeugVerbunden'] && !$phaseGueltig) { } elseif ($phaseErforderlich && !$phaseGueltig) {
$stoertext = 'Easee meldet keine unterstuetzte aktive Ausgangsphase.'; $stoertext = 'Easee meldet beim Laden keine unterstuetzte aktive Ausgangsphase.';
$this->SetStatus(self::STATUS_WARTET_AUF_DATEN); $this->SetStatus(self::STATUS_WARTET_AUF_DATEN);
} else { } else {
$this->SetStatus(self::STATUS_AKTIV); $this->SetStatus(self::STATUS_AKTIV);
@@ -471,7 +496,8 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if ($gatewayVerbunden && $status['StatusGueltig']) { if ($gatewayVerbunden && $status['StatusGueltig']) {
$this->setzeLadestrom( $this->setzeLadestrom(
$this->bestimmeWirksameSollleistung($angebot), $this->bestimmeWirksameSollleistung($angebot),
$status['Phasenzahl'] $regelPhasenzahl,
$status['Fahrzeugstatus'] === 2
); );
} else { } else {
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1); $this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
@@ -510,34 +536,64 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
return 0; return 0;
} }
private function setzeLadestrom(int $leistung, int $phasenzahl): void private function setzeLadestrom(
{ int $leistung,
int $phasenzahl,
bool $wartetAufStart
): void {
$strom = $leistung === 0 $strom = $leistung === 0
? 0 ? 0
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl); : LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) { if ($strom !== $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
return; $antwort = $this->gatewayAnfrage([
'action' => 'SetDynamicChargerCurrent',
'serialNumber' => $this->seriennummer(),
'amps' => $strom,
]);
if (!($antwort['success'] ?? false)) {
throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee-Stromvorgabe fehlgeschlagen.')
);
}
$befehl = json_encode([
'Aktion' => 'SetDynamicChargerCurrent',
'Seriennummer' => $this->seriennummer(),
'Ampere' => $strom,
], JSON_THROW_ON_ERROR);
$this->WriteAttributeString('LetzterGeraetebefehl', $befehl);
$this->setzeSichtbareVariable('LetzterGeraetebefehl', $befehl);
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
} }
if (
$strom > 0
&& $wartetAufStart
&& !$this->ReadAttributeBoolean('LadestartFuerVerbindungGesendet')
) {
$this->sendeLadestart();
}
}
private function sendeLadestart(): void
{
$antwort = $this->gatewayAnfrage([ $antwort = $this->gatewayAnfrage([
'action' => 'SetDynamicChargerCurrent', 'action' => 'StartCharging',
'serialNumber' => $this->seriennummer(), 'serialNumber' => $this->seriennummer(),
'amps' => $strom,
]); ]);
if (!($antwort['success'] ?? false)) { if (!($antwort['success'] ?? false)) {
throw new RuntimeException( throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee-Stromvorgabe fehlgeschlagen.') (string) ($antwort['error'] ?? 'Easee-Ladestart fehlgeschlagen.')
); );
} }
$befehl = json_encode([ $befehl = json_encode([
'Aktion' => 'SetDynamicChargerCurrent', 'Aktion' => 'StartCharging',
'Seriennummer' => $this->seriennummer(), 'Seriennummer' => $this->seriennummer(),
'Ampere' => $strom,
], JSON_THROW_ON_ERROR); ], JSON_THROW_ON_ERROR);
$this->WriteAttributeString('LetzterGeraetebefehl', $befehl); $this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', true);
$this->setzeSichtbareVariable('LetzterGeraetebefehl', $befehl); $this->WriteAttributeString('LetzterStartbefehl', $befehl);
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom); $this->setzeSichtbareVariable('LetzterStartbefehl', $befehl);
} }
private function seriennummer(): string private function seriennummer(): string
@@ -648,6 +704,36 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Wert' => $this->ReadAttributeBoolean('ZustandSolarladen'), 'Wert' => $this->ReadAttributeBoolean('ZustandSolarladen'),
'Einheit' => '', 'Einheit' => '',
], ],
[
'Kennung' => 'EaseeAktiviert',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('EaseeAktiviert'),
'Einheit' => '',
],
[
'Kennung' => 'AutorisierungErforderlich',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('AutorisierungErforderlich'),
'Einheit' => '',
],
[
'Kennung' => 'RemoteStartErforderlich',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('RemoteStartErforderlich'),
'Einheit' => '',
],
[
'Kennung' => 'SmartCharging',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('SmartCharging'),
'Einheit' => '',
],
[
'Kennung' => 'GrundKeinLadestrom',
'Art' => 'Status',
'Wert' => (int) $this->leseZustand('GrundKeinLadestrom'),
'Einheit' => '',
],
[ [
'Kennung' => 'Ladefehler', 'Kennung' => 'Ladefehler',
'Art' => 'Stoerung', 'Art' => 'Stoerung',
@@ -813,17 +899,53 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'', '',
210 210
); );
$this->RegisterVariableBoolean(
'EaseeAktiviert',
'Easee aktiviert',
'~Switch',
220
);
$this->RegisterVariableBoolean(
'AutorisierungErforderlich',
'Autorisierung erforderlich',
'~Switch',
230
);
$this->RegisterVariableBoolean(
'RemoteStartErforderlich',
'Remote-Start erforderlich',
'~Switch',
240
);
$this->RegisterVariableBoolean(
'SmartCharging',
'Easee Smart Charging',
'~Switch',
250
);
$this->RegisterVariableInteger(
'GrundKeinLadestrom',
'Grund kein Ladestrom',
'',
260
);
$this->RegisterVariableString( $this->RegisterVariableString(
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
'Letzter Geraetebefehl', 'Letzter Geraetebefehl',
'', '',
220 270
);
$this->RegisterVariableString(
'LetzterStartbefehl',
'Letzter Startbefehl',
'',
280
); );
$this->RegisterVariableString( $this->RegisterVariableString(
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
'Leistungsangebot', 'Leistungsangebot',
'', '',
230 290
); );
$this->SetValue( $this->SetValue(
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
@@ -833,11 +955,16 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
$this->ReadAttributeString('LetzterGeraetebefehl') $this->ReadAttributeString('LetzterGeraetebefehl')
); );
$this->SetValue(
'LetzterStartbefehl',
$this->ReadAttributeString('LetzterStartbefehl')
);
foreach (self::DIAGNOSE_VARIABLEN as $ident) { foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if (!in_array($ident, [ if (!in_array($ident, [
'GatewayVerbunden', 'GatewayVerbunden',
'ApiMaximalstrom', 'ApiMaximalstrom',
'LetzterGeraetebefehl', 'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose', 'LeistungsangebotDiagnose',
], true)) { ], true)) {
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident)); $this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
@@ -872,6 +999,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
switch ($ident) { switch ($ident) {
case 'FahrzeugVerbunden': case 'FahrzeugVerbunden':
case 'FahrzeugGeladen': case 'FahrzeugGeladen':
case 'EaseeAktiviert':
case 'AutorisierungErforderlich':
case 'RemoteStartErforderlich':
case 'SmartCharging':
case 'SollwertGueltig': case 'SollwertGueltig':
case 'Verfuegbar': case 'Verfuegbar':
case 'AenderungMoeglich': case 'AenderungMoeglich':
@@ -880,6 +1011,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
break; break;
case 'Fahrzeugstatus': case 'Fahrzeugstatus':
case 'Phasenzahl': case 'Phasenzahl':
case 'GrundKeinLadestrom':
case 'Sollleistung': case 'Sollleistung':
$this->WriteAttributeInteger('Zustand' . $ident, (int) $wert); $this->WriteAttributeInteger('Zustand' . $ident, (int) $wert);
break; break;
@@ -915,6 +1047,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($ident, [ if (in_array($ident, [
'FahrzeugVerbunden', 'FahrzeugVerbunden',
'FahrzeugGeladen', 'FahrzeugGeladen',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'SollwertGueltig', 'SollwertGueltig',
'Verfuegbar', 'Verfuegbar',
'AenderungMoeglich', 'AenderungMoeglich',
@@ -925,6 +1061,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($ident, [ if (in_array($ident, [
'Fahrzeugstatus', 'Fahrzeugstatus',
'Phasenzahl', 'Phasenzahl',
'GrundKeinLadestrom',
'Sollleistung', 'Sollleistung',
], true)) { ], true)) {
return $this->ReadAttributeInteger('Zustand' . $ident); return $this->ReadAttributeInteger('Zustand' . $ident);
+7 -1
View File
@@ -117,6 +117,10 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
$this->initialisiereLokaleEinstellungen(); $this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen(); $this->aktualisiereVariablen();
// Ein persistiertes Ladeende nach Neueinrichtung oder Modulupdate einmal neu pruefen.
$this->setzeZustand('FahrzeugGeladen', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
try { try {
$this->pruefeKonfiguration(); $this->pruefeKonfiguration();
} catch (Throwable $fehler) { } catch (Throwable $fehler) {
@@ -301,7 +305,9 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
); );
$status = LadestationRegler::normalisiereStatus( $status = LadestationRegler::normalisiereStatus(
$rohstatus, $rohstatus,
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden') $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden'),
$this->ReadAttributeBoolean('ZustandFahrzeugGeladen'),
$this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6
); );
foreach ($status as $ident => $wert) { foreach ($status as $ident => $wert) {
+17 -20
View File
@@ -689,22 +689,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
); );
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0); $this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
$this->regleEinspeisung($netzleistung, $stoerungen, $erneuern); $this->regleEinspeisung($netzleistung, $stoerungen, $erneuern);
if (!$synchronisiert) {
$this->setzeDiagnosewert('Verteilbudget', 0.0);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'Betriebsart' => $betriebsart,
'Synchronisiert' => false,
'Verbraucher' => $verbraucher,
'NichtGeregelt' => $nichtGeregelt,
], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert(
'NichtGeregelteGeraete',
$this->nichtGeregelteGeraeteText($nichtGeregelt)
);
$this->aktualisiereStoerungen($verbraucher, $stoerungen);
$this->SetStatus(self::STATUS_AKTIV);
return;
}
$fahrplanVorgabe = $this->aktuelleNetzfahrplanVorgabe(); $fahrplanVorgabe = $this->aktuelleNetzfahrplanVorgabe();
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK $zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
@@ -719,14 +703,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']); $this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([ $this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'Betriebsart' => $betriebsart, 'Betriebsart' => $betriebsart,
'Synchronisiert' => true, 'Synchronisiert' => $synchronisiert,
'Restabweichung_W' => $ergebnis['Restabweichung_W'], 'Restabweichung_W' => $ergebnis['Restabweichung_W'],
'Verbraucher' => $verbraucher, 'Verbraucher' => $verbraucher,
'NichtGeregelt' => [], 'NichtGeregelt' => $nichtGeregelt,
], JSON_THROW_ON_ERROR)); ], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert( $this->setzeDiagnosewert(
'NichtGeregelteGeraete', 'NichtGeregelteGeraete',
'Alle aktiven Verbraucher werden geregelt.' $nichtGeregelt === []
? 'Alle aktiven Verbraucher werden geregelt.'
: $this->nichtGeregelteGeraeteText($nichtGeregelt)
); );
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte'); $letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
@@ -1450,7 +1436,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken')); $token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
if ($token === '') { if ($token === '') {
throw new RuntimeException('Der Geraetezugang fuer die Stoerueberwachung fehlt.'); $this->aktualisiereLizenzfreigabe(
$this->ReadPropertyString('Lizenzcode'),
true
);
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
}
if ($token === '') {
$lizenzfehler = trim($this->ReadAttributeString('LizenzLetzterFehler'));
throw new RuntimeException(
'Der Geraetezugang fuer die Stoerueberwachung fehlt.'
. ($lizenzfehler !== '' ? ' Lizenzpruefung: ' . $lizenzfehler : '')
);
} }
$snapshot['capturedAt'] = gmdate('Y-m-d\TH:i:s\Z', $jetzt); $snapshot['capturedAt'] = gmdate('Y-m-d\TH:i:s\Z', $jetzt);
$this->uebertrageStoerungsSnapshot( $this->uebertrageStoerungsSnapshot(
+9 -5
View File
@@ -43,12 +43,16 @@ Ein Betriebsartwechsel wird synchronisiert:
3. `null` ist nur eine Ankuendigung und kein Schaltbefehl. 3. `null` ist nur eine Ankuendigung und kein Schaltbefehl.
4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu 4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu
und meldet es mit derselben Betriebsart zurueck. und meldet es mit derselben Betriebsart zurueck.
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der 5. Der Manager verteilt konkrete Sollleistungen an alle bereits
Manager konkrete Sollleistungen. synchronisierten Verbraucher.
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher bleiben von
der Verteilung ausgeschlossen und werden als Stoerung ausgewiesen.
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Eine Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Ein
technische Schaltsperre, eine ausstehende Rueckmeldung oder eine Mindestzeit nicht antwortender Verbraucher blockiert die Regelung der uebrigen Anlage
kann das Angebot weiterhin auf die aktuell gehaltene Leistung begrenzen. nicht; sein aktueller Einfluss ist bereits in der Netzleistungsmessung
enthalten. Eine technische Schaltsperre oder eine Mindestzeit kann das Angebot
weiterhin auf die aktuell gehaltene Leistung begrenzen.
| Verbraucher | PV-Angebot | Peak-Angebot | | Verbraucher | PV-Angebot | Peak-Angebot |
| --- | --- | --- | | --- | --- | --- |
+5 -2
View File
@@ -78,8 +78,11 @@ zulaessig ist.
`Sollleistung_W=null`. `Sollleistung_W=null`.
4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese 4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese
Betriebsart. Betriebsart.
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der Manager 5. Der Manager verteilt Sollleistungen an alle bereits synchronisierten
Sollleistungen. Verbraucher.
6. Fehlende, veraltete oder noch nicht umgeschaltete Verbraucher werden nicht
angesteuert und als Stoerung ausgewiesen; sie blockieren die aktuellen
Verbraucher nicht.
Damit kann jeder Verbrauchertyp unterschiedliche Angebote fuer PV und Peak Damit kann jeder Verbrauchertyp unterschiedliche Angebote fuer PV und Peak
melden, ohne dass der Manager seine interne Geraetelogik kennen muss. melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
+11 -5
View File
@@ -58,12 +58,15 @@ als ungueltig behandelt und ergeben bis zur gueltigen 1-/3-Phasenmeldung nur
Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`, Immer sichtbar sind `Aktiv`, `FahrzeugVerbunden`, `FahrzeugGeladen`,
`Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte `Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` bereit, `3` laedt, Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` wartet auf Start,
`4` geladen oder `5` Fehler. `3` laedt, `4` geladen oder `5` Fehler.
Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie
`GatewayVerbunden`, `ApiMaximalstrom`, `LetzterGeraetebefehl` und `GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
`LeistungsangebotDiagnose`. `AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
`LeistungsangebotDiagnose`. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
die Autorisierung fehlt.
## Regelung ## Regelung
@@ -83,7 +86,10 @@ Angebot sofort neu und meldet die Aenderung kurz gebuendelt an den Manager.
Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort Bei einer Gateway-Unterbrechung werden Fahrzeug- und Phasenstatus sofort
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0` frei. Die eigentliche Vorgabe wird als dynamischer Ladestrom mit `minutes=0`
an Easee gesendet. an Easee gesendet. Meldet Easee bei positiver Stromvorgabe weiterhin
`Awaiting Start`, sendet das Modul einmal pro Fahrzeugverbindung zusaetzlich
`start_charging`. Dieser Befehl autorisiert den Ladevorgang, uebersteuert aber
keinen vorhandenen Easee-Ladeplan.
## Managerkommunikation ## Managerkommunikation
+2 -1
View File
@@ -8,7 +8,8 @@ final class EaseeGatewayProtokoll
{ {
/** @var list<int> */ /** @var list<int> */
public const BEOBACHTUNGEN = [ public const BEOBACHTUNGEN = [
47, 48, 100, 104, 109, 110, 119, 120, 182, 183, 184, 185, 250, 31, 41, 42, 43, 47, 48, 62, 96, 100, 102, 104, 109, 110,
111, 112, 113, 114, 119, 120, 182, 183, 184, 185, 250,
]; ];
public static function seriennummer(string $wert): string public static function seriennummer(string $wert): string
+20
View File
@@ -22,6 +22,11 @@ final class EaseeLadestatus
* Istleistung_W: float, * Istleistung_W: float,
* Ladestrom_A: float, * Ladestrom_A: float,
* ApiMaximalstrom_A: int, * ApiMaximalstrom_A: int,
* EaseeAktiviert: bool,
* AutorisierungErforderlich: bool,
* RemoteStartErforderlich: bool,
* SmartCharging: bool,
* GrundKeinLadestrom: int,
* Stoerung: bool, * Stoerung: bool,
* Stoertext: string * Stoertext: string
* } * }
@@ -65,6 +70,16 @@ final class EaseeLadestatus
} }
} }
$apiMaximalstrom = max(6, min($grenzen)); $apiMaximalstrom = max(6, min($grenzen));
$easeeAktiviert = !array_key_exists('31', $beobachtungen)
|| (bool) $beobachtungen['31'];
$autorisierungErforderlich = (bool) (
$beobachtungen['42'] ?? $beobachtungen['41'] ?? false
);
$remoteStartErforderlich = (bool) ($beobachtungen['43'] ?? false);
$smartCharging = (bool) ($beobachtungen['102'] ?? false);
$grundKeinLadestrom = is_numeric($beobachtungen['96'] ?? null)
? (int) $beobachtungen['96']
: 0;
$fehlercode = (int) ($beobachtungen['119'] ?? 0); $fehlercode = (int) ($beobachtungen['119'] ?? 0);
$stoerung = $betriebszustand === 5 || $fehlercode !== 0; $stoerung = $betriebszustand === 5 || $fehlercode !== 0;
@@ -85,6 +100,11 @@ final class EaseeLadestatus
'Istleistung_W' => $leistung, 'Istleistung_W' => $leistung,
'Ladestrom_A' => $ladestrom, 'Ladestrom_A' => $ladestrom,
'ApiMaximalstrom_A' => $apiMaximalstrom, 'ApiMaximalstrom_A' => $apiMaximalstrom,
'EaseeAktiviert' => $easeeAktiviert,
'AutorisierungErforderlich' => $autorisierungErforderlich,
'RemoteStartErforderlich' => $remoteStartErforderlich,
'SmartCharging' => $smartCharging,
'GrundKeinLadestrom' => $grundKeinLadestrom,
'Stoerung' => $stoerung, 'Stoerung' => $stoerung,
'Stoertext' => trim($stoertext), 'Stoertext' => trim($stoertext),
]; ];
+8 -3
View File
@@ -34,8 +34,12 @@ final class LadestationRegler
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus * @param array{Rohstatus: int, Istleistung_W: float} $rohstatus
* @return array<string, int|float|bool> * @return array<string, int|float|bool>
*/ */
public static function normalisiereStatus(array $rohstatus, bool $warVerbunden): array public static function normalisiereStatus(
{ array $rohstatus,
bool $warVerbunden,
bool $warGeladen = false,
bool $ladungErwartet = true
): array {
$verbunden = $rohstatus['Rohstatus'] !== 1; $verbunden = $rohstatus['Rohstatus'] !== 1;
$leistung = max(0.0, $rohstatus['Istleistung_W']); $leistung = max(0.0, $rohstatus['Istleistung_W']);
$phasen = self::PHASEN_UNBEKANNT; $phasen = self::PHASEN_UNBEKANNT;
@@ -47,7 +51,8 @@ final class LadestationRegler
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0; $nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0;
$ladestrom = $leistung / $nenner; $ladestrom = $leistung / $nenner;
$ermittelterMaximalstrom = 2.5 + $ladestrom; $ermittelterMaximalstrom = 2.5 + $ladestrom;
$geladen = $warVerbunden && $ermittelterMaximalstrom < 6.0; $geladen = $warGeladen
|| ($warVerbunden && $ladungErwartet && $ermittelterMaximalstrom < 6.0);
} }
if (!$verbunden) { if (!$verbunden) {
+31
View File
@@ -45,6 +45,37 @@ final class EaseeGatewayProtokollTest extends TestCase
self::assertSame([], $beobachtungen); self::assertSame([], $beobachtungen);
} }
public function testRestBeobachtungenVerwendenAngefragteSeriennummer(): void
{
$beobachtungen = EaseeGatewayProtokoll::extrahiereBeobachtungen([
['id' => 109, 'value' => 1],
['observationId' => 110, 'value' => 30],
], ['EU2FGKEU'], 'EU2FGKEU');
self::assertSame([
['Seriennummer' => 'EU2FGKEU', 'ID' => 109, 'Wert' => 1],
['Seriennummer' => 'EU2FGKEU', 'ID' => 110, 'Wert' => 30],
], $beobachtungen);
}
public function testFreigabediagnoseWirdAusBeobachtungenExtrahiert(): void
{
$beobachtungen = EaseeGatewayProtokoll::extrahiereBeobachtungen([
'serialNumber' => 'EH123456',
'observations' => [
['id' => 42, 'value' => true],
['id' => 43, 'value' => true],
['id' => 96, 'value' => 55],
],
], ['EH123456']);
self::assertSame([
['Seriennummer' => 'EH123456', 'ID' => 42, 'Wert' => true],
['Seriennummer' => 'EH123456', 'ID' => 43, 'Wert' => true],
['Seriennummer' => 'EH123456', 'ID' => 96, 'Wert' => 55],
], $beobachtungen);
}
public function testSeriennummerWirdNormalisiert(): void public function testSeriennummerWirdNormalisiert(): void
{ {
self::assertSame( self::assertSame(
+18
View File
@@ -89,6 +89,24 @@ final class EaseeLadestatusTest extends TestCase
self::assertSame(13, $status['ApiMaximalstrom_A']); self::assertSame(13, $status['ApiMaximalstrom_A']);
} }
public function testFreigabeUndGrundOhneLadestromKommenAusEaseeBeobachtungen(): void
{
$status = EaseeLadestatus::ausBeobachtungen([
'31' => true,
'42' => true,
'43' => true,
'96' => 55,
'102' => true,
'109' => 2,
], 16);
self::assertTrue($status['EaseeAktiviert']);
self::assertTrue($status['AutorisierungErforderlich']);
self::assertTrue($status['RemoteStartErforderlich']);
self::assertTrue($status['SmartCharging']);
self::assertSame(55, $status['GrundKeinLadestrom']);
}
public function testNurSolarladenIstFestErzwungen(): void public function testNurSolarladenIstFestErzwungen(): void
{ {
self::assertFalse(EaseeLadestatus::solarladenErzwungen(0)); self::assertFalse(EaseeLadestatus::solarladenErzwungen(0));
+6
View File
@@ -70,6 +70,12 @@ final class EaseeModuleStrukturTest extends TestCase
self::assertStringContainsString("'Phasenzahl'", $modul); self::assertStringContainsString("'Phasenzahl'", $modul);
self::assertStringNotContainsString('7500', $modul); self::assertStringNotContainsString('7500', $modul);
self::assertStringNotContainsString('Abfrageintervall', $modul); self::assertStringNotContainsString('Abfrageintervall', $modul);
self::assertStringContainsString("'action' => 'StartCharging'", $modul);
$gateway = (string) file_get_contents(
__DIR__ . '/../EaseeGateway/module.php'
);
self::assertStringContainsString('/commands/start_charging', $gateway);
} }
/** @return array<string, mixed> */ /** @return array<string, mixed> */
+22
View File
@@ -44,6 +44,28 @@ final class LadestationReglerTest extends TestCase
self::assertSame(4, $status['Fahrzeugstatus']); self::assertSame(4, $status['Fahrzeugstatus']);
} }
public function testAbsichtlichGestoppteLadungWirdNichtAlsGeladenErkannt(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, false, false);
self::assertFalse($status['FahrzeugGeladen']);
self::assertSame(2, $status['Fahrzeugstatus']);
}
public function testErkanntesLadeendeBleibtBisZumAbsteckenErhalten(): void
{
$status = LadestationRegler::normalisiereStatus([
'Rohstatus' => 2,
'Istleistung_W' => 0.0,
], true, true, false);
self::assertTrue($status['FahrzeugGeladen']);
self::assertSame(4, $status['Fahrzeugstatus']);
}
public function testBetriebsartUndSolarladenBestimmenDasLeistungsangebot(): void public function testBetriebsartUndSolarladenBestimmenDasLeistungsangebot(): void
{ {
$solarPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, false); $solarPV = LadestationRegler::leistungsangebot(true, true, true, false, 1, 16, true, false);
+26
View File
@@ -254,6 +254,25 @@ final class ManagerModulstrukturTest extends TestCase
); );
} }
public function testManagerRegeltAktuelleVerbraucherTrotzFehlenderRueckmeldungen(): void
{
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
self::assertStringNotContainsString("if (!\$synchronisiert) {", $inhalt);
self::assertStringContainsString(
"'Synchronisiert' => \$synchronisiert",
$inhalt
);
self::assertStringContainsString(
"'NichtGeregelt' => \$nichtGeregelt",
$inhalt
);
self::assertStringContainsString(
"\$ergebnis = ManagerRegler::verteile(",
$inhalt
);
}
public function testManagerUebertraegtStoerungsvollsnapshotsMitRetry(): void public function testManagerUebertraegtStoerungsvollsnapshotsMitRetry(): void
{ {
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php'); $inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
@@ -280,6 +299,13 @@ final class ManagerModulstrukturTest extends TestCase
'"name":"StoermeldeAnschluss"', '"name":"StoermeldeAnschluss"',
str_replace(' ', '', (string) file_get_contents(__DIR__ . '/../Manager/form.json')) str_replace(' ', '', (string) file_get_contents(__DIR__ . '/../Manager/form.json'))
); );
self::assertStringContainsString(
"if (\$token === '') {\n"
. " \$this->aktualisiereLizenzfreigabe(\n"
. " \$this->ReadPropertyString('Lizenzcode'),\n"
. " true",
$inhalt
);
self::assertGreaterThanOrEqual( self::assertGreaterThanOrEqual(
3, 3,
substr_count( substr_count(
+28
View File
@@ -125,6 +125,34 @@ final class ManagerReglerTest extends TestCase
self::assertSame(0, $ergebnis['Restabweichung_W']); self::assertSame(0, $ergebnis['Restabweichung_W']);
} }
public function testVeralteterVerbraucherBlockiertUeberschussverteilungNicht(): void
{
$ergebnis = ManagerRegler::verteile(
-2411.0,
0.0,
[
$this->verbraucher(
44234,
0,
[['Von_W' => -39000, 'Bis_W' => 39000]],
0
),
$this->verbraucher(38676, 1, [0, 4600], 0),
$this->verbraucher(29743, 1, [0, 6500], 0),
$this->verbraucher(43878, 2, [13850], 10151, false),
$this->verbraucher(46052, 2, [11000], 0, false),
],
'PrioritaetPV'
);
self::assertSame(12562, $ergebnis['Verteilbudget_W']);
self::assertSame(
[44234 => 2411, 29743 => 0, 38676 => 0],
$ergebnis['Sollwerte']
);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
{ {
$verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false); $verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false);
+15
View File
@@ -72,6 +72,21 @@ return static function (TestContext $test): void {
$test->assertSame(200, $antwort['httpCode']); $test->assertSame(200, $antwort['httpCode']);
}); });
$test->runCase('Ladestart wird ueber Gateway angenommen', static function (
TestContext $test
) use ($gateway): void {
$antwort = json_decode(ENELIXEASEE_ProcessStationRequest(
$gateway,
json_encode([
'action' => 'StartCharging',
'serialNumber' => 'EH123456',
], JSON_THROW_ON_ERROR)
), true, 512, JSON_THROW_ON_ERROR);
$test->assertSame(true, $antwort['success']);
$test->assertSame(200, $antwort['httpCode']);
});
$test->runCase('Gebrochene Stromvorgabe wird abgelehnt', static function ( $test->runCase('Gebrochene Stromvorgabe wird abgelehnt', static function (
TestContext $test TestContext $test
) use ($gateway): void { ) use ($gateway): void {
@@ -108,6 +108,54 @@ return static function (TestContext $test): void {
)); ));
}); });
$test->runCase('Wartezustand startet konservativ ohne aktive Phase', static function (
TestContext $test
) use ($sendeBeobachtung, $station): void {
$sendeBeobachtung('EH123456', 42, true);
$sendeBeobachtung('EH123456', 43, true);
$sendeBeobachtung('EH123456', 96, 55);
$sendeBeobachtung('EH123456', 102, true);
$sendeBeobachtung('EH123456', 109, 2);
$sendeBeobachtung('EH123456', 110, 0);
IPS_RequestAction($station, 'Solarladen', false);
$test->assertSame(
0,
GetValue($test->objectByIdent('Phasenzahl', $station))
);
$test->assertInstanceStatus($station);
$test->assertSame(
'[3850]',
(string) GetValue(
$test->objectByIdent('LeistungsangebotDiagnose', $station)
)
);
$test->assertTrue(str_contains(
(string) GetValue(
$test->objectByIdent('LetzterGeraetebefehl', $station)
),
'"Ampere":16'
));
$test->assertTrue(str_contains(
(string) GetValue(
$test->objectByIdent('LetzterStartbefehl', $station)
),
'"Aktion":"StartCharging"'
));
$test->assertSame(
55,
GetValue($test->objectByIdent('GrundKeinLadestrom', $station))
);
$test->assertSame(
true,
GetValue($test->objectByIdent('AutorisierungErforderlich', $station))
);
IPS_RequestAction($station, 'Solarladen', true);
$sendeBeobachtung('EH123456', 110, 30);
$sendeBeobachtung('EH123456', 109, 3);
});
$test->runCase('Nur-Solar-Variante laesst Solarladen nicht abschalten', static function ( $test->runCase('Nur-Solar-Variante laesst Solarladen nicht abschalten', static function (
TestContext $test TestContext $test
) use ($nurSolar): void { ) use ($nurSolar): void {
@@ -61,6 +61,22 @@ return static function (TestContext $test): void {
); );
} }
$solarPause = $test->createInstance(
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
'Ladestation Solarpause',
[
'Geraetetyp' => 1,
'Geraeteadresse' => '192.0.2.12',
'MaximalerLadestrom' => 16,
'Ladefreigabe' => true,
'Solarladen' => true,
'EinstellungenInVisu' => true,
'DiagnosevariablenAnzeigen' => true,
'Testmodus' => true,
'Testantwort' => $goEAntwort(2, 0),
]
);
$managerId = $test->createInstance( $managerId = $test->createInstance(
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}', '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
'Hilfsmanager Ladestation', 'Hilfsmanager Ladestation',
@@ -120,6 +136,44 @@ return static function (TestContext $test): void {
} }
}); });
$test->runCase('Solarpause wird nicht als Ladeende erkannt', static function (
TestContext $test
) use ($solarPause): void {
IPS_RequestAction($solarPause, 'Solarladen', true);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
);
$test->assertSame(
2,
GetValue($test->objectByIdent('Fahrzeugstatus', $solarPause))
);
});
$test->runCase('ApplyChanges prueft ein gespeichertes Ladeende neu', static function (
TestContext $test
) use ($goEAntwort, $solarPause): void {
IPS_RequestAction($solarPause, 'Solarladen', false);
IPS_RequestAction($solarPause, 'Statuszyklus', false);
$test->assertSame(
true,
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
);
IPS_SetProperty($solarPause, 'Testantwort', $goEAntwort(2, 2300));
IPS_ApplyChanges($solarPause);
$test->assertSame(
false,
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
);
$test->assertSame(
3,
GetValue($test->objectByIdent('Fahrzeugstatus', $solarPause))
);
});
$test->runCase('Solar- und Peakbetrieb liefern die Enelix-1-Angebote', static function ( $test->runCase('Solar- und Peakbetrieb liefern die Enelix-1-Angebote', static function (
TestContext $test TestContext $test
) use ($stationen, $anforderungSenden): void { ) use ($stationen, $anforderungSenden): void {
+42
View File
@@ -444,4 +444,46 @@ return static function (TestContext $test): void {
$test->objectByIdent('Sammelstoerung', $managerId) $test->objectByIdent('Sammelstoerung', $managerId)
)); ));
}); });
$test->runCase('Veralteter Verbraucher blockiert aktuelle Verbraucher nicht', static function (
TestContext $test
) use (
$managerId,
$netzleistungId,
$einStufigId,
$warmwasserId,
$pufferspeicherId,
$melden,
$verbraucherstatus
): void {
IPS_SetProperty($managerId, 'VerbraucherTimeout', 1);
IPS_ApplyChanges($managerId);
SetValue($netzleistungId, -2400.0);
$melden();
IPS_RequestAction($managerId, 'Regeln', true);
$test->waitUntil(
static fn (): bool => ($verbraucherstatus()['Synchronisiert'] ?? false) === true
);
IPS_Sleep(1100);
IPS_RequestAction($warmwasserId, 'Melden', true);
IPS_RequestAction($pufferspeicherId, 'Melden', true);
IPS_RequestAction($managerId, 'Regeln', true);
$daten = $verbraucherstatus();
$test->assertSame(false, $daten['Synchronisiert'] ?? null);
$test->assertSame(2, count($daten['Verbraucher'] ?? []));
$test->assertSame(1, count($daten['NichtGeregelt'] ?? []));
$test->assertSame(
$einStufigId,
$daten['NichtGeregelt'][0]['InstanzID'] ?? null
);
$test->assertSame(
'Meldung ist veraltet',
$daten['NichtGeregelt'][0]['Grund'] ?? null
);
$test->assertTrue(
(float) GetValue($test->objectByIdent('Verteilbudget', $managerId)) > 0.0
);
});
}; };