Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae2cd8ecff | ||
|
|
995b704e36 | ||
|
|
5ff0d405d4 | ||
|
|
ccecc437e9 | ||
|
|
156591a91e | ||
|
|
e0814a0c14 | ||
|
|
3cabf597ac | ||
|
|
b86810d30f | ||
|
|
d1fc62d43c | ||
|
|
21577c0125 | ||
|
|
aefe46352f | ||
|
|
6f76e6cd86 |
@@ -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
@@ -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,
|
||||||
|
|||||||
+150
-13
@@ -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,15 +536,15 @@ 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([
|
$antwort = $this->gatewayAnfrage([
|
||||||
'action' => 'SetDynamicChargerCurrent',
|
'action' => 'SetDynamicChargerCurrent',
|
||||||
'serialNumber' => $this->seriennummer(),
|
'serialNumber' => $this->seriennummer(),
|
||||||
@@ -540,6 +566,36 @@ class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
|
|||||||
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
|
$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
|
private function seriennummer(): string
|
||||||
{
|
{
|
||||||
return EaseeGatewayProtokoll::seriennummer(
|
return EaseeGatewayProtokoll::seriennummer(
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+22
-27
@@ -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(
|
||||||
@@ -2266,12 +2263,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
/** @param array<string, mixed> $lease */
|
/** @param array<string, mixed> $lease */
|
||||||
private function prognoseBerechtigungInLease(array $lease): bool
|
private function prognoseBerechtigungInLease(array $lease): bool
|
||||||
{
|
{
|
||||||
return Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::NETZFAHRPLAN) > 0
|
return Lizenzpruefung::hatPrognoseBerechtigung($lease);
|
||||||
|| (Lizenzpruefung::berechtigungsMenge($lease, Lizenzpruefung::PROGNOSE_PV) > 0
|
|
||||||
&& Lizenzpruefung::berechtigungsMenge(
|
|
||||||
$lease,
|
|
||||||
Lizenzpruefung::PROGNOSE_VERBRAUCH
|
|
||||||
) > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool
|
private function berechtigungLizenziert(string $kennung, ?string $lizenzcode = null): bool
|
||||||
@@ -2421,7 +2413,10 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
if (!$lizenziert) {
|
if (!$lizenziert) {
|
||||||
return 'Prognosen sind deaktiviert. Fuer die Manager-ID '
|
return 'Prognosen sind deaktiviert. Fuer die Manager-ID '
|
||||||
. $managerID
|
. $managerID
|
||||||
. ' fehlen forecast_pv plus forecast_load oder grid_schedule.';
|
. ' ist keine Prognoseberechtigung vorhanden. Benoetigt wird entweder '
|
||||||
|
. 'PV-Ertragsprognose zusammen mit Verbrauchsprognose oder der '
|
||||||
|
. 'Intelligente Netzfahrplan. Nach der Erweiterung im Lizenzportal '
|
||||||
|
. 'die Lizenz erneut pruefen und binden.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ')
|
return ($aktiv ? 'Prognosen sind aktiviert. ' : 'Prognosen sind deaktiviert. ')
|
||||||
|
|||||||
@@ -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 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
+11
-2
@@ -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.
|
||||||
@@ -87,6 +90,12 @@ melden, ohne dass der Manager seine interne Geraetelogik kennen muss.
|
|||||||
## Feste Regeln
|
## Feste Regeln
|
||||||
|
|
||||||
- Prioritaeten beginnen bei 0; eine kleinere Zahl bedeutet hoehere Prioritaet.
|
- 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.
|
- `Leistungsquelle`: 0 nicht vorhanden, 1 berechnet, 2 gemessen.
|
||||||
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
|
- Bei Leistungsquelle 0 ist `Istleistung_W` zwingend `null`.
|
||||||
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
|
- Leistungsbereiche enthalten jeden ganzen Wattwert von `Von_W` bis `Bis_W`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ stabile ID gebunden werden.
|
|||||||
| HTTP `429` | Zu viele Aktivierungsversuche; vor dem naechsten Versuch warten. |
|
| HTTP `429` | Zu viele Aktivierungsversuche; vor dem naechsten Versuch warten. |
|
||||||
| `Lizenzserver nicht erreichbar` | DNS, Internetzugang, HTTPS und Systemzeit des Symcon-Systems pruefen. Eine bestehende Lease gilt nur bis `offlineUntil`. |
|
| `Lizenzserver nicht erreichbar` | DNS, Internetzugang, HTTPS und Systemzeit des Symcon-Systems pruefen. Eine bestehende Lease gilt nur bis `offlineUntil`. |
|
||||||
| `Offline-Freigabe ist abgelaufen.` | Verbindung zum Lizenzserver wiederherstellen und Lizenz erneut pruefen. |
|
| `Offline-Freigabe ist abgelaufen.` | Verbindung zum Lizenzserver wiederherstellen und Lizenz erneut pruefen. |
|
||||||
|
| `Keine Prognoseberechtigung vorhanden` | Im Lizenzportal entweder PV-Ertragsprognose zusammen mit Verbrauchsprognose oder den Intelligenten Netzfahrplan ergaenzen. Anschliessend im Manager `Lizenz pruefen und binden` ausloesen, die Prognose aktivieren und die Konfiguration speichern. |
|
||||||
|
|
||||||
Fuer eine genauere Diagnose koennen die Diagnosevariablen eingeblendet werden.
|
Fuer eine genauere Diagnose koennen die Diagnosevariablen eingeblendet werden.
|
||||||
`Lizenzstatus` zeigt dann den aktuellen Zustand. Mit aktiviertem Debug-Logging
|
`Lizenzstatus` zeigt dann den aktuellen Zustand. Mit aktiviertem Debug-Logging
|
||||||
@@ -302,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
|
Verbraucher. Eine gültige Istleistung wird bevorzugt; fehlt sie, bleibt der im
|
||||||
Zustand bestätigte Sollwert konservativ reserviert.
|
Zustand bestätigte Sollwert konservativ reserviert.
|
||||||
|
|
||||||
Nicht verfügbare, nicht änderbare oder aktuell angebotlose Verbraucher behalten
|
Nicht verfügbare oder aktuell angebotlose Verbraucher behalten ihre Leistung
|
||||||
ihre Leistung und erhalten keine neue Vorgabe. Die übrigen Verbraucher werden
|
und erhalten keine neue Vorgabe. Jeder verfügbare, synchronisierte Verbraucher
|
||||||
nach der gemeldeten PV- beziehungsweise Peak-Priorität und danach stabil nach
|
mit einem nicht leeren Angebot erhält dagegen einen Sollwert aus diesem Angebot.
|
||||||
Instanz-ID sortiert. Von ihrem jeweils kleinsten erlaubten Leistungswert aus
|
Das gilt auch bei `AenderungMoeglich=false`: Ein fixes Angebot wie `[11000]`
|
||||||
wird das Budget in dieser Reihenfolge aufgefüllt. Einzelwerte und ganzzahlige
|
wird mit genau `11000 W` zugeteilt.
|
||||||
Bereiche werden direkt verarbeitet; Lücken werden nie durch unzulässige Werte
|
|
||||||
geschlossen. Eine verbleibende Abweichung wird im `Verbraucherstatus`
|
Die Verbraucher werden nach der gemeldeten PV- beziehungsweise Peak-Priorität
|
||||||
dokumentiert.
|
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
|
Die Betriebsart wechselt unterhalb des Solar-Sollwerts zu `PV` und oberhalb
|
||||||
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
|
der wirksamen Lastspitzengrenze zu `Peak`. Zwischen den beiden Zielwerten
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -117,6 +117,19 @@ final class Lizenzpruefung
|
|||||||
return $menge;
|
return $menge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Prognose ist entweder durch den Netzfahrplan oder durch beide
|
||||||
|
* Einzelberechtigungen fuer PV und Verbrauch freigeschaltet.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $lease
|
||||||
|
*/
|
||||||
|
public static function hatPrognoseBerechtigung(array $lease): bool
|
||||||
|
{
|
||||||
|
return self::berechtigungsMenge($lease, self::NETZFAHRPLAN) > 0
|
||||||
|
|| (self::berechtigungsMenge($lease, self::PROGNOSE_PV) > 0
|
||||||
|
&& self::berechtigungsMenge($lease, self::PROGNOSE_VERBRAUCH) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rechnet freie, bestehende Einzellizenzen auf nicht separat lizenzierte Waermepumpen an.
|
* Rechnet freie, bestehende Einzellizenzen auf nicht separat lizenzierte Waermepumpen an.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ final class ManagerRegler
|
|||||||
$aktuelleLeistung += $leistung;
|
$aktuelleLeistung += $leistung;
|
||||||
|
|
||||||
$daten = $eintrag['Daten'];
|
$daten = $eintrag['Daten'];
|
||||||
if (!$daten['Verfuegbar'] || !$daten['AenderungMoeglich'] || $daten['Leistungswerte_W'] === []) {
|
if (!$daten['Verfuegbar'] || $daten['Leistungswerte_W'] === []) {
|
||||||
$nichtSteuerbar += $leistung;
|
$nichtSteuerbar += $leistung;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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> */
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -228,6 +228,47 @@ final class LizenzpruefungTest extends TestCase
|
|||||||
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
|
Lizenzpruefung::pruefeVerbrauchermengen($lease, $pruefmengen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPvUndVerbrauchGebenPrognoseFrei(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => Lizenzpruefung::PROGNOSE_PV,
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => Lizenzpruefung::PROGNOSE_VERBRAUCH,
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertTrue(Lizenzpruefung::hatPrognoseBerechtigung($lease));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEinzelnePrognoseBerechtigungReichtNichtAus(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => Lizenzpruefung::PROGNOSE_PV,
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertFalse(Lizenzpruefung::hatPrognoseBerechtigung($lease));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNetzfahrplanGibtPrognoseAlleineFrei(): void
|
||||||
|
{
|
||||||
|
$lease = $this->lease('manager_standard');
|
||||||
|
$lease['entitlements'][] = [
|
||||||
|
'catalog_key' => Lizenzpruefung::NETZFAHRPLAN,
|
||||||
|
'quantity' => 1,
|
||||||
|
'status' => 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertTrue(Lizenzpruefung::hatPrognoseBerechtigung($lease));
|
||||||
|
}
|
||||||
|
|
||||||
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
|
public function testLizenzcodeWirdWieAufDemServerNormalisiert(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -113,21 +113,55 @@ final class ManagerReglerTest extends TestCase
|
|||||||
self::assertSame(500, $ergebnis['Restabweichung_W']);
|
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);
|
$steuerbar = $this->verbraucher(20, 1, [0, ['Von_W' => 1, 'Bis_W' => 4000]], 0);
|
||||||
|
|
||||||
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
|
$ergebnis = ManagerRegler::verteile(3000.0, 5000.0, [$fest, $steuerbar], 'PrioritaetPV');
|
||||||
|
|
||||||
self::assertSame(3000, $ergebnis['NichtSteuerbar_W']);
|
self::assertSame(0, $ergebnis['NichtSteuerbar_W']);
|
||||||
self::assertSame([20 => 2000], $ergebnis['Sollwerte']);
|
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']);
|
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, [3000], null, false);
|
||||||
$verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN;
|
$verbraucher['Daten']['Leistungsquelle'] = Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN;
|
||||||
$verbraucher['Daten']['Istleistung_W'] = null;
|
$verbraucher['Daten']['Istleistung_W'] = null;
|
||||||
$verbraucher['Daten']['Zustand'][0]['Wert'] = 3000;
|
$verbraucher['Daten']['Zustand'][0]['Wert'] = 3000;
|
||||||
@@ -135,6 +169,7 @@ final class ManagerReglerTest extends TestCase
|
|||||||
$ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV');
|
$ergebnis = ManagerRegler::verteile(3000.0, 0.0, [$verbraucher], 'PrioritaetPV');
|
||||||
|
|
||||||
self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']);
|
self::assertSame(3000, $ergebnis['AktuelleVerbraucherleistung_W']);
|
||||||
|
self::assertSame([10 => 3000], $ergebnis['Sollwerte']);
|
||||||
self::assertSame(-3000, $ergebnis['Restabweichung_W']);
|
self::assertSame(-3000, $ergebnis['Restabweichung_W']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user