Merge remote-tracking branch 'origin/develop' into develop
Tests / test (push) Successful in 46s

This commit is contained in:
dh
2026-09-29 11:43:12 +00:00
19 changed files with 444 additions and 62 deletions
+3
View File
@@ -18,6 +18,9 @@ Alle wesentlichen Aenderungen an Enelix EMS werden in dieser Datei dokumentiert.
- Stufenlose Leistungsbereiche fuer Batteriespeicher.
- 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.
### Hinweise
+24
View File
@@ -221,6 +221,12 @@ class EaseeGateway extends IPSModule
$seriennummer,
(float) ($anfrage['amps'] ?? -1)
), JSON_THROW_ON_ERROR);
case 'StartCharging':
return json_encode(
$this->sendeLadestart($seriennummer),
JSON_THROW_ON_ERROR
);
}
return json_encode([
@@ -670,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> */
private function autorisierteApiAnfrage(
string $methode,
+144 -9
View File
@@ -41,7 +41,13 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Stoertext',
'GatewayVerbunden',
'ApiMaximalstrom',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose',
];
@@ -82,6 +88,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeString('EaseeBeobachtungen', '{}');
$this->RegisterAttributeInteger('LetzteBeobachtung', 0);
$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('ZustandFahrzeugGeladen', false);
$this->RegisterAttributeInteger('ZustandFahrzeugstatus', 0);
@@ -99,7 +110,9 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->RegisterAttributeString('LetzterGeraetebefehl', '');
$this->RegisterAttributeString('LetzterStartbefehl', '');
$this->RegisterTimer(
'Meldezyklus',
@@ -128,6 +141,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->aktualisiereVariablen();
$this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
try {
$this->pruefeKonfiguration();
@@ -405,6 +419,11 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Phasenzahl',
'Istleistung_W',
'Ladestrom_A',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
] as $feld) {
$this->setzeZustand($feld, $status[$feld]);
}
@@ -416,6 +435,9 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'ApiMaximalstrom',
$status['ApiMaximalstrom_A']
);
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
}
$gatewayVerbunden = $this->ReadAttributeBoolean('GatewayVerbunden');
$phaseGueltig = in_array($status['Phasenzahl'], [1, 3], true);
@@ -423,6 +445,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$phaseErforderlich = $status['Fahrzeugstatus'] === 3;
$steuerbar = $gatewayVerbunden
&& $status['StatusGueltig']
&& $status['EaseeAktiviert']
&& !$status['Stoerung']
&& (!$phaseErforderlich || $phaseGueltig);
@@ -473,7 +496,8 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if ($gatewayVerbunden && $status['StatusGueltig']) {
$this->setzeLadestrom(
$this->bestimmeWirksameSollleistung($angebot),
$regelPhasenzahl
$regelPhasenzahl,
$status['Fahrzeugstatus'] === 2
);
} else {
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
@@ -512,15 +536,15 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
return 0;
}
private function setzeLadestrom(int $leistung, int $phasenzahl): void
{
private function setzeLadestrom(
int $leistung,
int $phasenzahl,
bool $wartetAufStart
): void {
$strom = $leistung === 0
? 0
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
return;
}
if ($strom !== $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
$antwort = $this->gatewayAnfrage([
'action' => 'SetDynamicChargerCurrent',
'serialNumber' => $this->seriennummer(),
@@ -542,6 +566,36 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
}
if (
$strom > 0
&& $wartetAufStart
&& !$this->ReadAttributeBoolean('LadestartFuerVerbindungGesendet')
) {
$this->sendeLadestart();
}
}
private function sendeLadestart(): void
{
$antwort = $this->gatewayAnfrage([
'action' => 'StartCharging',
'serialNumber' => $this->seriennummer(),
]);
if (!($antwort['success'] ?? false)) {
throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee-Ladestart fehlgeschlagen.')
);
}
$befehl = json_encode([
'Aktion' => 'StartCharging',
'Seriennummer' => $this->seriennummer(),
], JSON_THROW_ON_ERROR);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', true);
$this->WriteAttributeString('LetzterStartbefehl', $befehl);
$this->setzeSichtbareVariable('LetzterStartbefehl', $befehl);
}
private function seriennummer(): string
{
return EaseeGatewayProtokoll::seriennummer(
@@ -650,6 +704,36 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'Wert' => $this->ReadAttributeBoolean('ZustandSolarladen'),
'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',
'Art' => 'Stoerung',
@@ -815,17 +899,53 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'',
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(
'LetzterGeraetebefehl',
'Letzter Geraetebefehl',
'',
220
270
);
$this->RegisterVariableString(
'LetzterStartbefehl',
'Letzter Startbefehl',
'',
280
);
$this->RegisterVariableString(
'LeistungsangebotDiagnose',
'Leistungsangebot',
'',
230
290
);
$this->SetValue(
'LeistungsangebotDiagnose',
@@ -835,11 +955,16 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
'LetzterGeraetebefehl',
$this->ReadAttributeString('LetzterGeraetebefehl')
);
$this->SetValue(
'LetzterStartbefehl',
$this->ReadAttributeString('LetzterStartbefehl')
);
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if (!in_array($ident, [
'GatewayVerbunden',
'ApiMaximalstrom',
'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose',
], true)) {
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
@@ -874,6 +999,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
switch ($ident) {
case 'FahrzeugVerbunden':
case 'FahrzeugGeladen':
case 'EaseeAktiviert':
case 'AutorisierungErforderlich':
case 'RemoteStartErforderlich':
case 'SmartCharging':
case 'SollwertGueltig':
case 'Verfuegbar':
case 'AenderungMoeglich':
@@ -882,6 +1011,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
break;
case 'Fahrzeugstatus':
case 'Phasenzahl':
case 'GrundKeinLadestrom':
case 'Sollleistung':
$this->WriteAttributeInteger('Zustand' . $ident, (int) $wert);
break;
@@ -917,6 +1047,10 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($ident, [
'FahrzeugVerbunden',
'FahrzeugGeladen',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'SollwertGueltig',
'Verfuegbar',
'AenderungMoeglich',
@@ -927,6 +1061,7 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
if (in_array($ident, [
'Fahrzeugstatus',
'Phasenzahl',
'GrundKeinLadestrom',
'Sollleistung',
], true)) {
return $this->ReadAttributeInteger('Zustand' . $ident);
+17 -20
View File
@@ -689,22 +689,6 @@ class Manager extends IPSModule implements ManagerSchnittstelle
);
$this->setzeDiagnosewert('WirksameLastspitzengrenze', $lastspitzengrenze ?? 0.0);
$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();
$zielnetzleistung = $betriebsart === ManagerRegler::BETRIEBSART_PEAK
@@ -719,14 +703,16 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$this->setzeDiagnosewert('Verteilbudget', (float) $ergebnis['Verteilbudget_W']);
$this->setzeDiagnosewert('Verbraucherstatus', json_encode([
'Betriebsart' => $betriebsart,
'Synchronisiert' => true,
'Synchronisiert' => $synchronisiert,
'Restabweichung_W' => $ergebnis['Restabweichung_W'],
'Verbraucher' => $verbraucher,
'NichtGeregelt' => [],
'NichtGeregelt' => $nichtGeregelt,
], JSON_THROW_ON_ERROR));
$this->setzeDiagnosewert(
'NichtGeregelteGeraete',
'Alle aktiven Verbraucher werden geregelt.'
$nichtGeregelt === []
? 'Alle aktiven Verbraucher werden geregelt.'
: $this->nichtGeregelteGeraeteText($nichtGeregelt)
);
$letzteSollwerte = $this->leseJsonAttribut('LetzteSollwerte');
@@ -1450,7 +1436,18 @@ class Manager extends IPSModule implements ManagerSchnittstelle
$token = trim($this->ReadAttributeString('PrognoseInstallationsToken'));
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);
$this->uebertrageStoerungsSnapshot(
+9 -5
View File
@@ -43,12 +43,16 @@ Ein Betriebsartwechsel wird synchronisiert:
3. `null` ist nur eine Ankuendigung und kein Schaltbefehl.
4. Jeder Verbraucher berechnet sein zustandsabhaengiges Leistungsangebot neu
und meldet es mit derselben Betriebsart zurueck.
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der
Manager konkrete Sollleistungen.
5. Der Manager verteilt konkrete Sollleistungen an alle bereits
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
technische Schaltsperre, eine ausstehende Rueckmeldung oder eine Mindestzeit
kann das Angebot weiterhin auf die aktuell gehaltene Leistung begrenzen.
Der Manager verwendet damit nie ein PV-Angebot fuer Peak oder umgekehrt. Ein
nicht antwortender Verbraucher blockiert die Regelung der uebrigen Anlage
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 |
| --- | --- | --- |
+11 -2
View File
@@ -78,8 +78,11 @@ zulaessig ist.
`Sollleistung_W=null`.
4. Jeder Verbraucher berechnet und meldet seine PowerSteps fuer diese
Betriebsart.
5. Erst wenn alle aktiven Verbraucher synchronisiert sind, verteilt der Manager
Sollleistungen.
5. Der Manager verteilt Sollleistungen an alle bereits synchronisierten
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
melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
@@ -87,6 +90,12 @@ melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
## Feste Regeln
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet.
- Jeder verfuegbare, synchronisierte Verbraucher mit einem nicht leeren
`Leistungswerte_W`-Angebot erhaelt einen Sollwert aus genau diesem Angebot.
- `AenderungMoeglich=false` kennzeichnet ein fixes Angebot und ist kein
Ausschlussgrund. Ein Angebot `[11000]` fuehrt deshalb zwingend zu `11000 W`.
- Prioritaeten verteilen nur die ueber den jeweiligen Mindestwert hinaus
verfuegbare Leistung; sie duerfen keinen angebotenen Mindestwert verdrängen.
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
+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`,
`Fahrzeugstatus`, `Ladestrom` und `Phasenzahl`. Der normalisierte
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` bereit, `3` laedt,
`4` geladen oder `5` Fehler.
Fahrzeugstatus ist `0` unbekannt, `1` getrennt, `2` wartet auf Start,
`3` laedt, `4` geladen oder `5` Fehler.
Optional sichtbar sind die gemeinsamen EMS-Diagnosewerte sowie
`GatewayVerbunden`, `ApiMaximalstrom`, `LetzterGeraetebefehl` und
`LeistungsangebotDiagnose`.
`GatewayVerbunden`, `ApiMaximalstrom`, `EaseeAktiviert`,
`AutorisierungErforderlich`, `RemoteStartErforderlich`, `SmartCharging`,
`GrundKeinLadestrom`, `LetzterGeraetebefehl`, `LetzterStartbefehl` und
`LeistungsangebotDiagnose`. `GrundKeinLadestrom=55` bedeutet bei Easee, dass
die Autorisierung fehlt.
## 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
verworfen; erst ein neuer aktueller Gateway-Zustand gibt die Regelung wieder
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
+13 -8
View File
@@ -303,14 +303,19 @@ der aktiven Betriebsart und der aktuellen Leistung aller frisch gemeldeten
Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im
Zustand bestätigte Sollwert konservativ reserviert.
Nicht verfügbare, nicht änderbare oder aktuell angebotlose Verbraucher behalten
ihre Leistung und erhalten keine neue Vorgabe. Die übrigen Verbraucher werden
nach der gemeldeten PV- beziehungsweise Peak-Priorität und danach stabil nach
Instanz-ID sortiert. Von ihrem jeweils kleinsten erlaubten Leistungswert aus
wird das Budget in dieser Reihenfolge aufgefüllt. Einzelwerte und ganzzahlige
Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte
geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus`
dokumentiert.
Nicht verfügbare oder aktuell angebotlose Verbraucher behalten ihre Leistung
und erhalten keine neue Vorgabe. Jeder verfügbare, synchronisierte Verbraucher
mit einem nicht leeren Angebot erhält dagegen einen Sollwert aus diesem Angebot.
Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
wird mit genau `11000 W` zugeteilt.
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
und danach stabil nach Instanz-ID sortiert. Zuerst reserviert der Manager für
jeden Verbraucher den kleinsten erlaubten Leistungswert. Nur das danach
verbleibende Budget wird in Prioritätsreihenfolge aufgefüllt. Einzelwerte und
ganzzahlige Bereiche werden direkt verarbeitet; Lücken werden nie durch
unzulässige Werte geschlossen. Eine verbleibende Abweichung wird im
`Verbraucherstatus` dokumentiert.
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
+2 -1
View File
@@ -8,7 +8,8 @@ final class EaseeGatewayProtokoll
{
/** @var list<int> */
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
+20
View File
@@ -22,6 +22,11 @@ final class EaseeLadestatus
* Istleistung_W: float,
* Ladestrom_A: float,
* ApiMaximalstrom_A: int,
* EaseeAktiviert: bool,
* AutorisierungErforderlich: bool,
* RemoteStartErforderlich: bool,
* SmartCharging: bool,
* GrundKeinLadestrom: int,
* Stoerung: bool,
* Stoertext: string
* }
@@ -65,6 +70,16 @@ final class EaseeLadestatus
}
}
$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);
$stoerung = $betriebszustand === 5 || $fehlercode !== 0;
@@ -85,6 +100,11 @@ final class EaseeLadestatus
'Istleistung_W' => $leistung,
'Ladestrom_A' => $ladestrom,
'ApiMaximalstrom_A' => $apiMaximalstrom,
'EaseeAktiviert' => $easeeAktiviert,
'AutorisierungErforderlich' => $autorisierungErforderlich,
'RemoteStartErforderlich' => $remoteStartErforderlich,
'SmartCharging' => $smartCharging,
'GrundKeinLadestrom' => $grundKeinLadestrom,
'Stoerung' => $stoerung,
'Stoertext' => trim($stoertext),
];
+1 -1
View File
@@ -120,7 +120,7 @@ final class ManagerRegler
$aktuelleLeistung += $leistung;
$daten = $eintrag['Daten'];
if (!$daten['Verfuegbar'] || !$daten['AenderungMoeglich'] || $daten['Leistungswerte_W'] === []) {
if (!$daten['Verfuegbar'] || $daten['Leistungswerte_W'] === []) {
$nichtSteuerbar += $leistung;
continue;
}
+18
View File
@@ -58,6 +58,24 @@ final class EaseeGatewayProtokollTest extends TestCase
], $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
{
self::assertSame(
+18
View File
@@ -89,6 +89,24 @@ final class EaseeLadestatusTest extends TestCase
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
{
self::assertFalse(EaseeLadestatus::solarladenErzwungen(0));
+6
View File
@@ -70,6 +70,12 @@ final class EaseeModuleStrukturTest extends TestCase
self::assertStringContainsString("'Phasenzahl'", $modul);
self::assertStringNotContainsString('7500', $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> */
+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
{
$inhalt = (string) file_get_contents(__DIR__ . '/../Manager/module.php');
@@ -280,6 +299,13 @@ final class ManagerModulstrukturTest extends TestCase
'"name":"StoermeldeAnschluss"',
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(
3,
substr_count(
+40 -5
View File
@@ -113,21 +113,55 @@ final class ManagerReglerTest extends TestCase
self::assertSame(500, $ergebnis['Restabweichung_W']);
}
public function testNichtAenderbarerVerbraucherReserviertSeineIstleistung(): void
public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
{
$fest = $this->verbraucher(10, 0, [0, 3000], 3000, false);
$fest = $this->verbraucher(10, 0, [3000], 3000, false);
$steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0);
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
self::assertSame(3000, $ergebnis['NichtSteuerbar_W']);
self::assertSame([20 => 2000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['NichtSteuerbar_W']);
self::assertSame([10 => 3000, 20 => 2000], $ergebnis['Sollwerte']);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testFestwerteWerdenMitHoeherPriorisierterBatterieZugewiesen(): 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 => -12288,
29743 => 0,
38676 => 0,
43878 => 13850,
46052 => 11000,
],
$ergebnis['Sollwerte']
);
self::assertSame(0, $ergebnis['Restabweichung_W']);
}
public function testFehlendeIstleistungFaelltAufAngenommenenSollwertZurueck(): void
{
$verbraucher = $this->verbraucher(10, 0, [0, 3000], null, false);
$verbraucher = $this->verbraucher(10, 0, [3000], null, false);
$verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN;
$verbraucher['Daten']['Istleistung_W'] = null;
$verbraucher['Daten']['Zustand'][0]['Wert'] = 3000;
@@ -135,6 +169,7 @@ final class ManagerReglerTest extends TestCase
$ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV');
self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']);
self::assertSame([10 => 3000], $ergebnis['Sollwerte']);
self::assertSame(-3000, $ergebnis['Restabweichung_W']);
}
+15
View File
@@ -72,6 +72,21 @@ return static function (TestContext $test): void {
$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 (
TestContext $test
) use ($gateway): void {
@@ -111,6 +111,10 @@ 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);
@@ -132,6 +136,20 @@ return static function (TestContext $test): void {
),
'"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);
+42
View File
@@ -444,4 +444,46 @@ return static function (TestContext $test): void {
$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
);
});
};