Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8357e8dfab | ||
|
|
38353aed93 | ||
|
|
cf92e7623b | ||
|
|
add1077674 | ||
|
|
bfdcf4536b | ||
|
|
faa5dfb33f | ||
|
|
53fca5e2bb | ||
|
|
f611da975d | ||
|
|
393969594e | ||
|
|
cae14cf399 | ||
|
|
3aaa744a8a | ||
|
|
fdc2007bb1 | ||
|
|
37400878b6 | ||
|
|
ace1821213 | ||
|
|
b3b868cd2a | ||
|
|
a74f14aa19 | ||
|
|
fe966516b5 | ||
|
|
b3bbae1a72 | ||
|
|
ae2cd8ecff | ||
|
|
995b704e36 | ||
|
|
5ff0d405d4 | ||
|
|
ccecc437e9 | ||
|
|
156591a91e | ||
|
|
e0814a0c14 | ||
|
|
3cabf597ac | ||
|
|
b86810d30f | ||
|
|
d1fc62d43c | ||
|
|
21577c0125 | ||
|
|
aefe46352f | ||
|
|
6f76e6cd86 |
@@ -17,6 +17,13 @@ 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.
|
||||||
|
- Stand-alone-Ladestationen erkennen nach einer einstellbaren Beobachtungszeit den niedrigeren Fahrzeug-Maximalstrom und begrenzen ihr Leistungsangebot ohne Ladeunterbruch.
|
||||||
|
- 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.
|
||||||
|
- Verbraucher gleicher Prioritaet werden wie in Enelix 1 anhand ihrer bezogenen Energie in 2-kWh-Gruppen ausgeglichen.
|
||||||
|
- Die Stoerungsueberwachung fordert einen fehlenden Geraetezugang innerhalb ihres Retry-Zyklus automatisch neu an.
|
||||||
|
- Der Warmwassererwaermer verwendet konfigurierbare Temperatur-Stoergrenzen von standardmaessig 0 bis 100 Grad C.
|
||||||
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
|
- Vollstaendige PHP-, Struktur- und Symcon-Modultests fuer alle Module.
|
||||||
|
|
||||||
### Hinweise
|
### Hinweise
|
||||||
|
|||||||
+202
-29
@@ -16,6 +16,7 @@ class EaseeGateway extends IPSModule
|
|||||||
private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers';
|
private const SIGNALR_BASE_URL = 'https://streams.easee.com/hubs/chargers';
|
||||||
private const API_BASE_URL = 'https://api.easee.com';
|
private const API_BASE_URL = 'https://api.easee.com';
|
||||||
private const RECORD_SEPARATOR = "\x1e";
|
private const RECORD_SEPARATOR = "\x1e";
|
||||||
|
private const CACHE_MAXIMALALTER_SEKUNDEN = 60;
|
||||||
|
|
||||||
public function Create(): void
|
public function Create(): void
|
||||||
{
|
{
|
||||||
@@ -50,6 +51,11 @@ class EaseeGateway extends IPSModule
|
|||||||
0,
|
0,
|
||||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);"
|
"IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);"
|
||||||
);
|
);
|
||||||
|
$this->RegisterTimer(
|
||||||
|
'ReconnectTimer',
|
||||||
|
0,
|
||||||
|
"IPS_RequestAction(\$_IPS['TARGET'], 'ReconnectAfterLoss', false);"
|
||||||
|
);
|
||||||
|
|
||||||
$this->RequireParent(self::WEBSOCKET_MODULE_ID);
|
$this->RequireParent(self::WEBSOCKET_MODULE_ID);
|
||||||
}
|
}
|
||||||
@@ -60,6 +66,7 @@ class EaseeGateway extends IPSModule
|
|||||||
|
|
||||||
$this->SetTimerInterval('MaintainConnectionTimer', 0);
|
$this->SetTimerInterval('MaintainConnectionTimer', 0);
|
||||||
$this->SetTimerInterval('TokenRefreshTimer', 0);
|
$this->SetTimerInterval('TokenRefreshTimer', 0);
|
||||||
|
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||||
$this->SetBuffer('SignalRReady', '0');
|
$this->SetBuffer('SignalRReady', '0');
|
||||||
$this->SetBuffer('ReceiveBuffer', '');
|
$this->SetBuffer('ReceiveBuffer', '');
|
||||||
$this->SetBuffer('Subscriptions', '{}');
|
$this->SetBuffer('Subscriptions', '{}');
|
||||||
@@ -111,8 +118,22 @@ class EaseeGateway extends IPSModule
|
|||||||
$this->pflegeVerbindung();
|
$this->pflegeVerbindung();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'ReconnectAfterLoss':
|
||||||
|
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||||
|
if (
|
||||||
|
$this->ReadPropertyBoolean('Active')
|
||||||
|
&& !$this->ReadPropertyBoolean('Testmodus')
|
||||||
|
&& $this->GetBuffer('SignalRReady') !== '1'
|
||||||
|
) {
|
||||||
|
$this->verbindeSignalR();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
case 'RefreshToken':
|
case 'RefreshToken':
|
||||||
if ($this->erneuereZugangsdaten(false)) {
|
if (
|
||||||
|
$this->erneuereZugangsdaten(false)
|
||||||
|
&& $this->GetBuffer('SignalRReady') !== '1'
|
||||||
|
) {
|
||||||
$this->verbindeSignalR();
|
$this->verbindeSignalR();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -142,6 +163,15 @@ class EaseeGateway extends IPSModule
|
|||||||
$daten['value'] ?? null
|
$daten['value'] ?? null
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case 'TestSignalRFrame':
|
||||||
|
if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'TestSignalRFrame ist nur im Testmodus zulaessig.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->verarbeiteSignalRRahmen($wert);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||||
@@ -221,6 +251,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([
|
||||||
@@ -271,8 +307,10 @@ class EaseeGateway extends IPSModule
|
|||||||
if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) {
|
if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->SetBuffer('SignalRReady', '0');
|
$this->planeWiederverbindung(
|
||||||
$this->setzeVerbunden(false);
|
'Seit mehr als 90 Sekunden keine Easee-Ereignisdaten empfangen.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($jetzt - $letzteAushandlung) >= 45) {
|
if (($jetzt - $letzteAushandlung) >= 45) {
|
||||||
@@ -285,16 +323,22 @@ class EaseeGateway extends IPSModule
|
|||||||
|
|
||||||
private function verbindeSignalR(): bool
|
private function verbindeSignalR(): bool
|
||||||
{
|
{
|
||||||
|
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||||
$this->SetStatus(102);
|
$this->setzeEreignisverbindung(true);
|
||||||
$this->setzeVerbunden(true);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!$this->stelleAccessTokenSicher()) {
|
if (!$this->stelleAccessTokenSicher()) {
|
||||||
|
$this->SetBuffer('SignalRReady', '0');
|
||||||
|
$this->setzeVerbunden(false);
|
||||||
$this->SetStatus(202);
|
$this->SetStatus(202);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->setzeEreignisverbindung(
|
||||||
|
false,
|
||||||
|
'Easee-Ereignisverbindung wird aufgebaut.'
|
||||||
|
);
|
||||||
$this->SetBuffer('LastNegotiation', (string) time());
|
$this->SetBuffer('LastNegotiation', (string) time());
|
||||||
$token = $this->GetBuffer('AccessToken');
|
$token = $this->GetBuffer('AccessToken');
|
||||||
$antwort = $this->httpAnfrage(
|
$antwort = $this->httpAnfrage(
|
||||||
@@ -313,8 +357,8 @@ class EaseeGateway extends IPSModule
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!$antwort['success']) {
|
if (!$antwort['success']) {
|
||||||
$this->SetStatus(203);
|
$this->setzeEreignisverbindung(
|
||||||
$this->setzeLetztenFehler(
|
false,
|
||||||
'Easee-Ereignisverbindung fehlgeschlagen: HTTP '
|
'Easee-Ereignisverbindung fehlgeschlagen: HTTP '
|
||||||
. $antwort['httpCode']
|
. $antwort['httpCode']
|
||||||
. ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '')
|
. ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '')
|
||||||
@@ -327,8 +371,10 @@ class EaseeGateway extends IPSModule
|
|||||||
? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '')
|
? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '')
|
||||||
: '';
|
: '';
|
||||||
if ($verbindungstoken === '') {
|
if ($verbindungstoken === '') {
|
||||||
$this->SetStatus(203);
|
$this->setzeEreignisverbindung(
|
||||||
$this->setzeLetztenFehler('Easee liefert kein Verbindungstoken.');
|
false,
|
||||||
|
'Easee liefert kein Verbindungstoken.'
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +386,7 @@ class EaseeGateway extends IPSModule
|
|||||||
$this->SetBuffer('ReceiveBuffer', '');
|
$this->SetBuffer('ReceiveBuffer', '');
|
||||||
$this->SetBuffer('SubscribedThisConnection', '{}');
|
$this->SetBuffer('SubscribedThisConnection', '{}');
|
||||||
$this->SetBuffer('PendingInvocations', '{}');
|
$this->SetBuffer('PendingInvocations', '{}');
|
||||||
$this->WriteAttributeString('ObservationCache', '{}');
|
$this->markiereBeobachtungscacheVeraltet();
|
||||||
$this->setzeVerbunden(false);
|
$this->setzeVerbunden(false);
|
||||||
|
|
||||||
if (!$this->konfiguriereWebSocketParent($url)) {
|
if (!$this->konfiguriereWebSocketParent($url)) {
|
||||||
@@ -403,10 +449,7 @@ class EaseeGateway extends IPSModule
|
|||||||
private function verarbeiteSignalRRahmen(string $rahmen): void
|
private function verarbeiteSignalRRahmen(string $rahmen): void
|
||||||
{
|
{
|
||||||
if ($rahmen === '{}') {
|
if ($rahmen === '{}') {
|
||||||
$this->SetBuffer('SignalRReady', '1');
|
$this->setzeEreignisverbindung(true);
|
||||||
$this->SetStatus(102);
|
|
||||||
$this->setzeLetztenFehler('');
|
|
||||||
$this->setzeVerbunden(true);
|
|
||||||
$this->sendeAlleAbonnements();
|
$this->sendeAlleAbonnements();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -417,8 +460,9 @@ class EaseeGateway extends IPSModule
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isset($nachricht['error'])) {
|
if (isset($nachricht['error'])) {
|
||||||
$this->SetStatus(203);
|
$this->planeWiederverbindung(
|
||||||
$this->setzeLetztenFehler('Easee-Ereignisfehler: ' . $nachricht['error']);
|
'Easee-Ereignisfehler: ' . $nachricht['error']
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +471,9 @@ class EaseeGateway extends IPSModule
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($typ === 7) {
|
if ($typ === 7) {
|
||||||
$this->SetBuffer('SignalRReady', '0');
|
$this->planeWiederverbindung(
|
||||||
$this->setzeVerbunden(false);
|
'Easee-Ereignisverbindung wurde beendet.'
|
||||||
$this->setzeLetztenFehler('Easee-Ereignisverbindung wurde beendet.');
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +574,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 +608,84 @@ 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 markiereBeobachtungscacheVeraltet(): void
|
||||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
{
|
||||||
'Buffer' => json_encode([
|
$cache = $this->leseAttributArray('ObservationCache');
|
||||||
'type' => 'Observation',
|
foreach ($cache as &$zustand) {
|
||||||
'serialNumber' => $seriennummer,
|
if (is_array($zustand)) {
|
||||||
'id' => $id,
|
$zustand['updated'] = 0;
|
||||||
'value' => $wert,
|
}
|
||||||
'timestamp' => time(),
|
}
|
||||||
], JSON_THROW_ON_ERROR),
|
unset($zustand);
|
||||||
], JSON_THROW_ON_ERROR));
|
$this->WriteAttributeString(
|
||||||
|
'ObservationCache',
|
||||||
|
json_encode($cache, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stelleAktuellenStatusSicher(string $seriennummer): void
|
||||||
|
{
|
||||||
|
$cache = $this->leseAttributArray('ObservationCache');
|
||||||
|
$zustand = $cache[$seriennummer] ?? [];
|
||||||
|
$cacheIstAktuell = is_array($zustand)
|
||||||
|
&& array_key_exists('109', $zustand)
|
||||||
|
&& (int) ($zustand['updated'] ?? 0) >= time() - self::CACHE_MAXIMALALTER_SEKUNDEN;
|
||||||
|
if (
|
||||||
|
$cacheIstAktuell
|
||||||
|
|| $this->ReadPropertyBoolean('Testmodus')
|
||||||
|
|| $this->GetBuffer('SignalRReady') !== '1'
|
||||||
|
) {
|
||||||
|
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 +732,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,
|
||||||
@@ -758,9 +902,38 @@ class EaseeGateway extends IPSModule
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function setzeEreignisverbindung(
|
||||||
|
bool $verbunden,
|
||||||
|
string $fehler = ''
|
||||||
|
): void {
|
||||||
|
$this->SetBuffer('SignalRReady', $verbunden ? '1' : '0');
|
||||||
|
$this->SetStatus($verbunden ? 102 : 203);
|
||||||
|
$this->setzeLetztenFehler($verbunden ? '' : $fehler);
|
||||||
|
if ($verbunden) {
|
||||||
|
$this->SetTimerInterval('ReconnectTimer', 0);
|
||||||
|
}
|
||||||
|
$this->setzeVerbunden($verbunden);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function planeWiederverbindung(string $fehler): void
|
||||||
|
{
|
||||||
|
$this->setzeEreignisverbindung(false, $fehler);
|
||||||
|
$this->SetBuffer('LastNegotiation', '0');
|
||||||
|
if (
|
||||||
|
$this->ReadPropertyBoolean('Active')
|
||||||
|
&& !$this->ReadPropertyBoolean('Testmodus')
|
||||||
|
) {
|
||||||
|
$this->SetTimerInterval('ReconnectTimer', 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function setzeVerbunden(bool $verbunden): void
|
private function setzeVerbunden(bool $verbunden): void
|
||||||
{
|
{
|
||||||
|
$warVerbunden = (bool) $this->GetValue('Connected');
|
||||||
$this->SetValue('Connected', $verbunden);
|
$this->SetValue('Connected', $verbunden);
|
||||||
|
if ($warVerbunden === $verbunden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->SendDataToChildren(json_encode([
|
$this->SendDataToChildren(json_encode([
|
||||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
'DataID' => self::CHILD_EVENT_DATA_ID,
|
||||||
'Buffer' => json_encode([
|
'Buffer' => json_encode([
|
||||||
|
|||||||
+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);
|
||||||
|
|||||||
@@ -37,6 +37,41 @@
|
|||||||
"caption": "Geraetestatus abfragen",
|
"caption": "Geraetestatus abfragen",
|
||||||
"suffix": " s",
|
"suffix": " s",
|
||||||
"minimum": 1
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Phasenerkennungszeit",
|
||||||
|
"caption": "Phasen erkennen nach",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "FahrzeugstromErkennungszeit",
|
||||||
|
"caption": "Fahrzeug-Maximalstrom erkennen nach",
|
||||||
|
"suffix": " s",
|
||||||
|
"minimum": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "ZeitZwischenZustandswechseln",
|
||||||
|
"caption": "Zeit zwischen Leistungsaenderungen",
|
||||||
|
"suffix": " min",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Mindesteinschaltdauer",
|
||||||
|
"caption": "Mindesteinschaltdauer",
|
||||||
|
"suffix": " min",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "Mindestausschaltdauer",
|
||||||
|
"caption": "Mindestausschaltdauer",
|
||||||
|
"suffix": " min",
|
||||||
|
"minimum": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
'AenderungMoeglich',
|
'AenderungMoeglich',
|
||||||
'Stoerung',
|
'Stoerung',
|
||||||
'Stoertext',
|
'Stoertext',
|
||||||
|
'FahrzeugMaximalstrom',
|
||||||
'LetzterGeraetebefehl',
|
'LetzterGeraetebefehl',
|
||||||
'LeistungsangebotDiagnose',
|
'LeistungsangebotDiagnose',
|
||||||
];
|
];
|
||||||
@@ -50,6 +51,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$this->RegisterPropertyString('Benutzername', '');
|
$this->RegisterPropertyString('Benutzername', '');
|
||||||
$this->RegisterPropertyString('Passwort', '');
|
$this->RegisterPropertyString('Passwort', '');
|
||||||
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
|
||||||
|
$this->RegisterPropertyInteger('Phasenerkennungszeit', 60);
|
||||||
|
$this->RegisterPropertyInteger('FahrzeugstromErkennungszeit', 90);
|
||||||
|
$this->RegisterPropertyInteger('ZeitZwischenZustandswechseln', 1);
|
||||||
|
$this->RegisterPropertyInteger('Mindesteinschaltdauer', 0);
|
||||||
|
$this->RegisterPropertyInteger('Mindestausschaltdauer', 0);
|
||||||
$this->RegisterPropertyInteger('Abfrageintervall', 5);
|
$this->RegisterPropertyInteger('Abfrageintervall', 5);
|
||||||
$this->RegisterPropertyBoolean('Ladefreigabe', true);
|
$this->RegisterPropertyBoolean('Ladefreigabe', true);
|
||||||
$this->RegisterPropertyBoolean('Solarladen', true);
|
$this->RegisterPropertyBoolean('Solarladen', true);
|
||||||
@@ -87,6 +93,16 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$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->RegisterAttributeInteger('LetzteGesetzteLeistung', 0);
|
||||||
|
$this->RegisterAttributeInteger('LeistungswechselGesperrtBis', 0);
|
||||||
|
$this->RegisterAttributeInteger('AusschaltenGesperrtBis', 0);
|
||||||
|
$this->RegisterAttributeInteger('EinschaltenGesperrtBis', 0);
|
||||||
|
$this->RegisterAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
$this->RegisterAttributeInteger('PhasenerkennungVersion', 0);
|
||||||
|
$this->RegisterAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||||
|
$this->RegisterAttributeInteger('ErkannterMaximalstrom', 32);
|
||||||
|
$this->RegisterAttributeInteger('FahrzeugstromKandidat', 0);
|
||||||
|
$this->RegisterAttributeInteger('FahrzeugstromKandidatSeit', 0);
|
||||||
|
|
||||||
$this->RegisterTimer(
|
$this->RegisterTimer(
|
||||||
'Statuszyklus',
|
'Statuszyklus',
|
||||||
@@ -117,6 +133,28 @@ 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);
|
||||||
|
$this->WriteAttributeInteger('LetzteGesetzteLeistung', 0);
|
||||||
|
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
if ($this->ReadAttributeInteger('PhasenerkennungVersion') < 1) {
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'ZustandPhasenzahl',
|
||||||
|
LadestationRegler::PHASEN_UNBEKANNT
|
||||||
|
);
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungVersion', 1);
|
||||||
|
}
|
||||||
|
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'ErkannterMaximalstrom',
|
||||||
|
$this->ReadPropertyInteger('MaximalerLadestrom')
|
||||||
|
);
|
||||||
|
$this->setzeFahrzeugstromKandidatZurueck();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pruefeKonfiguration();
|
$this->pruefeKonfiguration();
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
@@ -239,7 +277,11 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
|
$verbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
|
||||||
$geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen');
|
$geladen = $this->ReadAttributeBoolean('ZustandFahrzeugGeladen');
|
||||||
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
|
$phasenzahl = $this->ReadAttributeInteger('ZustandPhasenzahl');
|
||||||
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
$maximalstrom = min(
|
||||||
|
$this->ReadPropertyInteger('MaximalerLadestrom'),
|
||||||
|
max(6, $this->ReadAttributeInteger('ErkannterMaximalstrom'))
|
||||||
|
);
|
||||||
|
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $maximalstrom);
|
||||||
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
|
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
|
||||||
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
|
||||||
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
@@ -263,6 +305,40 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$solarladen,
|
$solarladen,
|
||||||
$peakbetrieb
|
$peakbetrieb
|
||||||
);
|
);
|
||||||
|
if ($aktiv
|
||||||
|
&& $ladefreigabe
|
||||||
|
&& $verbunden
|
||||||
|
&& !$geladen
|
||||||
|
&& !($peakbetrieb && $solarladen)
|
||||||
|
&& $phasenzahl !== LadestationRegler::PHASEN_UNBEKANNT
|
||||||
|
) {
|
||||||
|
$jetzt = time();
|
||||||
|
$letzteLeistung = $this->ReadAttributeInteger('LetzteGesetzteLeistung');
|
||||||
|
if ($jetzt < $this->ReadAttributeInteger('LeistungswechselGesperrtBis')) {
|
||||||
|
$angebot = [$letzteLeistung];
|
||||||
|
} elseif ($letzteLeistung > 0
|
||||||
|
&& $jetzt < $this->ReadAttributeInteger('AusschaltenGesperrtBis')
|
||||||
|
) {
|
||||||
|
$positiveStufen = array_values(array_filter(
|
||||||
|
LadestationRegler::leistungsangebot(
|
||||||
|
$aktiv,
|
||||||
|
$ladefreigabe,
|
||||||
|
$verbunden,
|
||||||
|
$geladen,
|
||||||
|
$phasenzahl,
|
||||||
|
$maximalstrom,
|
||||||
|
$solarladen,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
static fn (int $wert): bool => $wert > 0
|
||||||
|
));
|
||||||
|
$angebot = $positiveStufen !== [] ? $positiveStufen : [$letzteLeistung];
|
||||||
|
} elseif ($letzteLeistung <= 0
|
||||||
|
&& $jetzt < $this->ReadAttributeInteger('EinschaltenGesperrtBis')
|
||||||
|
) {
|
||||||
|
$angebot = [0];
|
||||||
|
}
|
||||||
|
}
|
||||||
$angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR);
|
$angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR);
|
||||||
$this->WriteAttributeString('Leistungsangebot', $angebotJson);
|
$this->WriteAttributeString('Leistungsangebot', $angebotJson);
|
||||||
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
|
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
|
||||||
@@ -270,7 +346,14 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
|
$this->setzeZustand('AenderungMoeglich', $verfuegbar && count($angebot) > 1);
|
||||||
|
|
||||||
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
$sollleistung = $this->bestimmeWirksameSollleistung($angebot);
|
||||||
|
if (!($aktiv
|
||||||
|
&& $ladefreigabe
|
||||||
|
&& $verbunden
|
||||||
|
&& !$geladen
|
||||||
|
&& $phasenzahl === LadestationRegler::PHASEN_UNBEKANNT
|
||||||
|
)) {
|
||||||
$this->setzeLadestrom($sollleistung);
|
$this->setzeLadestrom($sollleistung);
|
||||||
|
}
|
||||||
$this->setzeStoerung('');
|
$this->setzeStoerung('');
|
||||||
$this->SetStatus(self::STATUS_AKTIV);
|
$this->SetStatus(self::STATUS_AKTIV);
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
@@ -299,16 +382,126 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$this->ReadPropertyInteger('Geraetetyp'),
|
$this->ReadPropertyInteger('Geraetetyp'),
|
||||||
$antwort
|
$antwort
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$warVerbunden = $this->ReadAttributeBoolean('ZustandFahrzeugVerbunden');
|
||||||
|
$verbunden = $rohstatus['Rohstatus'] !== 1;
|
||||||
|
$bekanntePhasenzahl = $warVerbunden
|
||||||
|
? $this->ReadAttributeInteger('ZustandPhasenzahl')
|
||||||
|
: LadestationRegler::PHASEN_UNBEKANNT;
|
||||||
|
|
||||||
|
if (!$verbunden) {
|
||||||
|
$bekanntePhasenzahl = LadestationRegler::PHASEN_UNBEKANNT;
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
|
||||||
|
} elseif ($bekanntePhasenzahl === LadestationRegler::PHASEN_UNBEKANNT) {
|
||||||
|
$direktErkanntePhasen = (int) ($rohstatus['Phasenzahl']
|
||||||
|
?? LadestationRegler::PHASEN_UNBEKANNT);
|
||||||
|
if (in_array(
|
||||||
|
$direktErkanntePhasen,
|
||||||
|
[LadestationRegler::PHASEN_EIN, LadestationRegler::PHASEN_DREI],
|
||||||
|
true
|
||||||
|
)) {
|
||||||
|
$bekanntePhasenzahl = $direktErkanntePhasen;
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
} elseif ($rohstatus['Istleistung_W'] > 7500.0) {
|
||||||
|
$bekanntePhasenzahl = LadestationRegler::PHASEN_DREI;
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
} elseif ((bool) $this->GetValue('Aktiv')
|
||||||
|
&& $this->ReadAttributeBoolean('ZustandLadefreigabe')
|
||||||
|
) {
|
||||||
|
$erkennungSeit = $this->ReadAttributeInteger('PhasenerkennungSeit');
|
||||||
|
if ($erkennungSeit <= 0) {
|
||||||
|
$this->sendeLadestromAnGeraet(
|
||||||
|
$this->ReadPropertyInteger('MaximalerLadestrom')
|
||||||
|
);
|
||||||
|
$erkennungSeit = time();
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', $erkennungSeit);
|
||||||
|
} elseif ((time() - $erkennungSeit)
|
||||||
|
>= $this->ReadPropertyInteger('Phasenerkennungszeit')
|
||||||
|
) {
|
||||||
|
$bekanntePhasenzahl = $rohstatus['Istleistung_W'] > 7500.0
|
||||||
|
? LadestationRegler::PHASEN_DREI
|
||||||
|
: LadestationRegler::PHASEN_EIN;
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->WriteAttributeInteger('PhasenerkennungSeit', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$status = LadestationRegler::normalisiereStatus(
|
$status = LadestationRegler::normalisiereStatus(
|
||||||
$rohstatus,
|
$rohstatus,
|
||||||
$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden')
|
$warVerbunden,
|
||||||
|
$this->ReadAttributeBoolean('ZustandFahrzeugGeladen'),
|
||||||
|
$this->ReadAttributeInteger('LetzterGesetzterStrom') >= 6,
|
||||||
|
$this->ReadAttributeBoolean('LadeleistungSeitFreigabeErkannt'),
|
||||||
|
$bekanntePhasenzahl
|
||||||
);
|
);
|
||||||
|
if (!$status['FahrzeugVerbunden']) {
|
||||||
|
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||||
|
} elseif ($status['Ladestrom_A'] >= 3.5) {
|
||||||
|
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', true);
|
||||||
|
}
|
||||||
|
if (!$status['FahrzeugVerbunden']
|
||||||
|
|| $status['Phasenzahl'] !== LadestationRegler::PHASEN_UNBEKANNT
|
||||||
|
) {
|
||||||
|
$this->aktualisiereFahrzeugstromgrenze($status);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($status as $ident => $wert) {
|
foreach ($status as $ident => $wert) {
|
||||||
$this->setzeZustand($ident, $wert);
|
$this->setzeZustand($ident, $wert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, int|float|bool> $status */
|
||||||
|
private function aktualisiereFahrzeugstromgrenze(array $status): void
|
||||||
|
{
|
||||||
|
$konfigurierterMaximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
||||||
|
if (!$status['FahrzeugVerbunden']) {
|
||||||
|
$this->WriteAttributeInteger('ErkannterMaximalstrom', $konfigurierterMaximalstrom);
|
||||||
|
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $konfigurierterMaximalstrom);
|
||||||
|
$this->setzeFahrzeugstromKandidatZurueck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$kandidat = LadestationRegler::ermittleFahrzeugMaximalstrom(
|
||||||
|
$this->ReadAttributeInteger('LetzterGesetzterStrom'),
|
||||||
|
(float) $status['Ladestrom_A'],
|
||||||
|
$konfigurierterMaximalstrom
|
||||||
|
);
|
||||||
|
if ($kandidat === null
|
||||||
|
|| $kandidat >= $this->ReadAttributeInteger('ErkannterMaximalstrom')
|
||||||
|
) {
|
||||||
|
$this->setzeFahrzeugstromKandidatZurueck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jetzt = time();
|
||||||
|
if ($this->ReadAttributeInteger('FahrzeugstromKandidat') !== $kandidat) {
|
||||||
|
$this->WriteAttributeInteger('FahrzeugstromKandidat', $kandidat);
|
||||||
|
$this->WriteAttributeInteger('FahrzeugstromKandidatSeit', $jetzt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (($jetzt - $this->ReadAttributeInteger('FahrzeugstromKandidatSeit'))
|
||||||
|
< $this->ReadPropertyInteger('FahrzeugstromErkennungszeit')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger('ErkannterMaximalstrom', $kandidat);
|
||||||
|
$this->WriteAttributeInteger('LeistungswechselGesperrtBis', 0);
|
||||||
|
$this->setzeSichtbareVariable('FahrzeugMaximalstrom', $kandidat);
|
||||||
|
$this->setzeFahrzeugstromKandidatZurueck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeFahrzeugstromKandidatZurueck(): void
|
||||||
|
{
|
||||||
|
$this->WriteAttributeInteger('FahrzeugstromKandidat', 0);
|
||||||
|
$this->WriteAttributeInteger('FahrzeugstromKandidatSeit', 0);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param list<int> $angebot */
|
/** @param list<int> $angebot */
|
||||||
private function bestimmeWirksameSollleistung(array $angebot): int
|
private function bestimmeWirksameSollleistung(array $angebot): int
|
||||||
{
|
{
|
||||||
@@ -318,6 +511,14 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
return $sollleistung;
|
return $sollleistung;
|
||||||
}
|
}
|
||||||
$this->setzeZustand('SollwertGueltig', false);
|
$this->setzeZustand('SollwertGueltig', false);
|
||||||
|
$ersatz = 0;
|
||||||
|
foreach ($angebot as $leistungswert) {
|
||||||
|
if ($leistungswert > $sollleistung) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$ersatz = $leistungswert;
|
||||||
|
}
|
||||||
|
return $ersatz;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) {
|
if (!$this->ReadAttributeBoolean('ZustandSolarladen') && $angebot !== [0]) {
|
||||||
@@ -333,12 +534,61 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$strom = $leistung === 0
|
$strom = $leistung === 0
|
||||||
? 0
|
? 0
|
||||||
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
|
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
|
||||||
if ($strom === $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
|
$bisherigeLeistung = $this->ReadAttributeInteger('LetzteGesetzteLeistung');
|
||||||
|
$stromUnveraendert = $strom === $this->ReadAttributeInteger('LetzterGesetzterStrom');
|
||||||
|
|
||||||
|
if ($stromUnveraendert && $leistung === $bisherigeLeistung) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$stromUnveraendert) {
|
||||||
|
$this->sendeLadestromAnGeraet($strom);
|
||||||
|
}
|
||||||
|
if ($leistung === $bisherigeLeistung) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->ReadAttributeBoolean('ZustandFahrzeugVerbunden')) {
|
||||||
|
$leistungswechselGesperrtBis = 0;
|
||||||
|
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
|
||||||
|
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
|
||||||
|
} else {
|
||||||
|
$jetzt = time();
|
||||||
|
$wechselzeit = $this->ReadPropertyInteger('ZeitZwischenZustandswechseln');
|
||||||
|
$leistungswechselGesperrtBis = $wechselzeit > 0
|
||||||
|
? $jetzt + ($wechselzeit * 60)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if ($bisherigeLeistung <= 0 && $leistung > 0) {
|
||||||
|
$mindestzeit = $this->ReadPropertyInteger('Mindesteinschaltdauer');
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'AusschaltenGesperrtBis',
|
||||||
|
$mindestzeit > 0 ? $jetzt + ($mindestzeit * 60) : 0
|
||||||
|
);
|
||||||
|
$this->WriteAttributeInteger('EinschaltenGesperrtBis', 0);
|
||||||
|
} elseif ($bisherigeLeistung > 0 && $leistung <= 0) {
|
||||||
|
$mindestzeit = $this->ReadPropertyInteger('Mindestausschaltdauer');
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'EinschaltenGesperrtBis',
|
||||||
|
$mindestzeit > 0 ? $jetzt + ($mindestzeit * 60) : 0
|
||||||
|
);
|
||||||
|
$this->WriteAttributeInteger('AusschaltenGesperrtBis', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger('LetzteGesetzteLeistung', $leistung);
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'LeistungswechselGesperrtBis',
|
||||||
|
$leistungswechselGesperrtBis
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendeLadestromAnGeraet(int $strom): void
|
||||||
|
{
|
||||||
|
$this->WriteAttributeBoolean('LadeleistungSeitFreigabeErkannt', false);
|
||||||
|
$this->setzeFahrzeugstromKandidatZurueck();
|
||||||
|
|
||||||
$transport = new LadestationTransport(
|
$transport = new LadestationTransport(
|
||||||
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage)
|
fn (array $anfrage): string => $this->fuehreHttpAnfrageAus($anfrage, true)
|
||||||
);
|
);
|
||||||
$transport->ladestromSetzen(
|
$transport->ladestromSetzen(
|
||||||
$this->ReadPropertyInteger('Geraetetyp'),
|
$this->ReadPropertyInteger('Geraetetyp'),
|
||||||
@@ -351,8 +601,12 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $anfrage
|
* @param array<string, mixed> $anfrage
|
||||||
*/
|
*/
|
||||||
private function fuehreHttpAnfrageAus(array $anfrage): string
|
private function fuehreHttpAnfrageAus(
|
||||||
|
array $anfrage,
|
||||||
|
bool $alsGeraetebefehl = false
|
||||||
|
): string
|
||||||
{
|
{
|
||||||
|
if ($alsGeraetebefehl) {
|
||||||
$protokoll = [
|
$protokoll = [
|
||||||
'Methode' => $anfrage['Methode'],
|
'Methode' => $anfrage['Methode'],
|
||||||
'URL' => $anfrage['URL'],
|
'URL' => $anfrage['URL'],
|
||||||
@@ -361,6 +615,7 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
'LetzterGeraetebefehl',
|
'LetzterGeraetebefehl',
|
||||||
json_encode($protokoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
|
json_encode($protokoll, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||||
$url = (string) $anfrage['URL'];
|
$url = (string) $anfrage['URL'];
|
||||||
@@ -571,11 +826,27 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
throw new InvalidArgumentException($property . ' muss mindestens 0 sein.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach (['Meldeintervall', 'VorgabeTimeout', 'Abfrageintervall'] as $property) {
|
foreach ([
|
||||||
|
'Meldeintervall',
|
||||||
|
'VorgabeTimeout',
|
||||||
|
'Abfrageintervall',
|
||||||
|
'Phasenerkennungszeit',
|
||||||
|
'FahrzeugstromErkennungszeit',
|
||||||
|
] as $property) {
|
||||||
if ($this->ReadPropertyInteger($property) <= 0) {
|
if ($this->ReadPropertyInteger($property) <= 0) {
|
||||||
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
throw new InvalidArgumentException($property . ' muss groesser als 0 sein.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
foreach ([
|
||||||
|
'ZeitZwischenZustandswechseln',
|
||||||
|
'Mindesteinschaltdauer',
|
||||||
|
'Mindestausschaltdauer',
|
||||||
|
] as $property) {
|
||||||
|
if ($this->ReadPropertyInteger($property) < 0) {
|
||||||
|
throw new InvalidArgumentException($property . ' darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
|
||||||
if ($maximalstrom < 6 || $maximalstrom > 32) {
|
if ($maximalstrom < 6 || $maximalstrom > 32) {
|
||||||
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||||
@@ -627,12 +898,26 @@ class LadestationStandAlone extends IPSModule implements VerbraucherSchnittstell
|
|||||||
$this->registriereVerbraucherDiagnose();
|
$this->registriereVerbraucherDiagnose();
|
||||||
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200);
|
$this->RegisterVariableString('LetzterGeraetebefehl', 'Letzter Geraetebefehl', '', 200);
|
||||||
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210);
|
$this->RegisterVariableString('LeistungsangebotDiagnose', 'Leistungsangebot', '', 210);
|
||||||
|
$this->RegisterVariableInteger(
|
||||||
|
'FahrzeugMaximalstrom',
|
||||||
|
'Erkannter Fahrzeug-Maximalstrom',
|
||||||
|
'',
|
||||||
|
220
|
||||||
|
);
|
||||||
|
$this->SetValue(
|
||||||
|
'FahrzeugMaximalstrom',
|
||||||
|
$this->ReadAttributeInteger('ErkannterMaximalstrom')
|
||||||
|
);
|
||||||
$this->SetValue(
|
$this->SetValue(
|
||||||
'LeistungsangebotDiagnose',
|
'LeistungsangebotDiagnose',
|
||||||
$this->ReadAttributeString('Leistungsangebot')
|
$this->ReadAttributeString('Leistungsangebot')
|
||||||
);
|
);
|
||||||
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
|
||||||
if (!in_array($ident, ['LetzterGeraetebefehl', 'LeistungsangebotDiagnose'], true)) {
|
if (!in_array($ident, [
|
||||||
|
'LetzterGeraetebefehl',
|
||||||
|
'LeistungsangebotDiagnose',
|
||||||
|
'FahrzeugMaximalstrom',
|
||||||
|
], true)) {
|
||||||
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
|
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -742,6 +742,15 @@
|
|||||||
"name": "EnergieflussAnzeigen",
|
"name": "EnergieflussAnzeigen",
|
||||||
"caption": "Energiefluss unter dem Manager anzeigen",
|
"caption": "Energiefluss unter dem Manager anzeigen",
|
||||||
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
|
"onChange": "IPS_RequestAction($id, 'FormEnergieflussAnzeigen', $EnergieflussAnzeigen);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Select",
|
||||||
|
"name": "EnergieflussLeistungseinheit",
|
||||||
|
"caption": "Leistungseinheit im Energiefluss",
|
||||||
|
"options": [
|
||||||
|
{"caption": "Watt (W)", "value": 0},
|
||||||
|
{"caption": "Kilowatt (kW)", "value": 1}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+128
-65
@@ -173,6 +173,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
|
$this->RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false);
|
||||||
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
|
$this->RegisterPropertyBoolean('FunFactsAnzeigen', false);
|
||||||
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
|
$this->RegisterPropertyBoolean('EnergieflussAnzeigen', false);
|
||||||
|
$this->RegisterPropertyInteger('EnergieflussLeistungseinheit', 0);
|
||||||
|
|
||||||
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
|
$this->RegisterVariableBoolean('Aktiv', 'Aktiv', '~Switch', 10);
|
||||||
$this->EnableAction('Aktiv');
|
$this->EnableAction('Aktiv');
|
||||||
@@ -180,6 +181,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
|
$this->RegisterVariableFloat('Netzleistung', 'Netzleistung', '', 30);
|
||||||
|
|
||||||
$this->RegisterAttributeString('VerbraucherCache', '{}');
|
$this->RegisterAttributeString('VerbraucherCache', '{}');
|
||||||
|
$this->RegisterAttributeString('VerbraucherEnergie', '{}');
|
||||||
$this->RegisterAttributeString('LetzteSollwerte', '{}');
|
$this->RegisterAttributeString('LetzteSollwerte', '{}');
|
||||||
$this->RegisterAttributeString('LetztePvGrenzen', '{}');
|
$this->RegisterAttributeString('LetztePvGrenzen', '{}');
|
||||||
$this->RegisterAttributeString('Netzfahrplan', '{}');
|
$this->RegisterAttributeString('Netzfahrplan', '{}');
|
||||||
@@ -266,8 +268,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
}
|
}
|
||||||
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
$this->setzeDiagnosewert('Monatsgrenzen', $this->ReadPropertyString('Monatsgrenzen'));
|
||||||
$this->aktualisiereAnschlussstatus();
|
$this->aktualisiereAnschlussstatus();
|
||||||
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv')
|
$geraetezugangFehlt = $this->geraetezugangBenoetigt()
|
||||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
|
||||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||||
if (!$this->aktualisiereLizenzfreigabe(
|
if (!$this->aktualisiereLizenzfreigabe(
|
||||||
$this->ReadPropertyString('Lizenzcode'),
|
$this->ReadPropertyString('Lizenzcode'),
|
||||||
@@ -431,6 +432,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
|
if ((int) $senderID === $this->ReadPropertyInteger('NetzleistungVariableID')
|
||||||
&& (int) $nachricht === self::VM_UPDATE
|
&& (int) $nachricht === self::VM_UPDATE
|
||||||
) {
|
) {
|
||||||
|
$this->aktualisiereEnergieflusswerte();
|
||||||
$this->regeln(false);
|
$this->regeln(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +502,9 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
if (!is_string($wert)) {
|
if (!is_string($wert)) {
|
||||||
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
|
throw new InvalidArgumentException('Der Lizenzcode muss als Text uebergeben werden.');
|
||||||
}
|
}
|
||||||
$this->aktualisiereLizenzfreigabe($wert, true);
|
if ($this->aktualisiereLizenzfreigabe($wert, true)) {
|
||||||
|
$this->synchronisiereAnlagentopologie();
|
||||||
|
}
|
||||||
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
|
$this->UpdateFormField('LizenzInformation', 'caption', $this->lizenzStatustext($wert));
|
||||||
$this->aktualisiereLastspitzenformular($wert);
|
$this->aktualisiereLastspitzenformular($wert);
|
||||||
$this->aktualisierePrognoseformular($wert);
|
$this->aktualisierePrognoseformular($wert);
|
||||||
@@ -602,15 +606,79 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
|
throw new InvalidArgumentException('Der Absender ist diesem Manager nicht aktiv zugeordnet.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$jetzt = time();
|
||||||
|
$this->aktualisiereVerbraucherEnergie($absenderID, $daten, $jetzt);
|
||||||
|
|
||||||
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||||
$cache[(string) $absenderID] = [
|
$cache[(string) $absenderID] = [
|
||||||
'EmpfangenAm' => time(),
|
'EmpfangenAm' => $jetzt,
|
||||||
'Daten' => $daten,
|
'Daten' => $daten,
|
||||||
];
|
];
|
||||||
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
|
$this->WriteAttributeString('VerbraucherCache', json_encode($cache, JSON_THROW_ON_ERROR));
|
||||||
|
$this->aktualisiereEnergieflusswerte();
|
||||||
$this->regeln(false);
|
$this->regeln(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $daten */
|
||||||
|
private function aktualisiereVerbraucherEnergie(int $instanzID, array $daten, int $jetzt): void
|
||||||
|
{
|
||||||
|
$energien = $this->leseJsonAttribut('VerbraucherEnergie');
|
||||||
|
$bisher = $energien[(string) $instanzID] ?? [];
|
||||||
|
$energieWh = is_array($bisher) && is_numeric($bisher['Energie_Wh'] ?? null)
|
||||||
|
? (float) $bisher['Energie_Wh']
|
||||||
|
: 0.0;
|
||||||
|
$letzterZeitpunkt = is_array($bisher) && is_int($bisher['Zeitpunkt'] ?? null)
|
||||||
|
? $bisher['Zeitpunkt']
|
||||||
|
: 0;
|
||||||
|
$letzteLeistung = is_array($bisher) && is_numeric($bisher['Leistung_W'] ?? null)
|
||||||
|
? (float) $bisher['Leistung_W']
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
if ($letzterZeitpunkt > 0 && $jetzt > $letzterZeitpunkt) {
|
||||||
|
$dauer = min(
|
||||||
|
$jetzt - $letzterZeitpunkt,
|
||||||
|
$this->ReadPropertyInteger('VerbraucherTimeout')
|
||||||
|
);
|
||||||
|
$energieWh += $letzteLeistung * $dauer / 3600.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$energien[(string) $instanzID] = [
|
||||||
|
'Energie_Wh' => $energieWh,
|
||||||
|
'Zeitpunkt' => $jetzt,
|
||||||
|
'Leistung_W' => $this->verbraucherleistung($daten),
|
||||||
|
];
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'VerbraucherEnergie',
|
||||||
|
json_encode($energien, JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $daten */
|
||||||
|
private function verbraucherleistung(array $daten): float
|
||||||
|
{
|
||||||
|
if ($daten['Leistungsquelle'] !== Nachrichtenvertrag::LEISTUNGSQUELLE_NICHT_VORHANDEN
|
||||||
|
&& is_numeric($daten['Istleistung_W'])
|
||||||
|
) {
|
||||||
|
return (float) $daten['Istleistung_W'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, float> */
|
||||||
|
private function bezogeneVerbraucherEnergienWh(): array
|
||||||
|
{
|
||||||
|
$ergebnis = [];
|
||||||
|
foreach ($this->leseJsonAttribut('VerbraucherEnergie') as $instanzID => $eintrag) {
|
||||||
|
if (!is_array($eintrag) || !is_numeric($eintrag['Energie_Wh'] ?? null)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ergebnis[(int) $instanzID] = (float) $eintrag['Energie_Wh'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
private function regeln(bool $erneuern): void
|
private function regeln(bool $erneuern): void
|
||||||
{
|
{
|
||||||
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
|
if (!$this->aktualisiereLizenzfreigabe($this->ReadPropertyString('Lizenzcode'), false)) {
|
||||||
@@ -689,22 +757,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
|
||||||
@@ -714,19 +766,22 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$netzleistung,
|
$netzleistung,
|
||||||
$zielnetzleistung,
|
$zielnetzleistung,
|
||||||
$verbraucher,
|
$verbraucher,
|
||||||
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV'
|
$betriebsart === ManagerRegler::BETRIEBSART_PEAK ? 'PrioritaetPeak' : 'PrioritaetPV',
|
||||||
|
$this->bezogeneVerbraucherEnergienWh()
|
||||||
);
|
);
|
||||||
$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 +1505,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(
|
||||||
@@ -2058,6 +2124,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
|
if (is_int($funFactsID) && $funFactsID > 0 && IPS_VariableExists($funFactsID)) {
|
||||||
$this->aktualisiereFunFacts($funFactsID);
|
$this->aktualisiereFunFacts($funFactsID);
|
||||||
}
|
}
|
||||||
|
$this->aktualisiereEnergieflusswerte();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function istNumerischeVariable(int $variableID): bool
|
private function istNumerischeVariable(int $variableID): bool
|
||||||
@@ -2126,8 +2193,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
&& $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
&& $this->lizenzBerechtigungenPassen($lease, $lastspitzenmodus);
|
||||||
$gueltig = $leaseGueltig && $berechtigungenPassen;
|
$gueltig = $leaseGueltig && $berechtigungenPassen;
|
||||||
$berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen;
|
$berechtigungenVeraltet = $basisGueltig && !$berechtigungenPassen;
|
||||||
$geraetezugangFehlt = ($this->ReadPropertyBoolean('PrognoseAktiv')
|
$geraetezugangFehlt = $this->geraetezugangBenoetigt()
|
||||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
|
||||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '';
|
||||||
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
$routineFaellig = (!$gueltig && !$berechtigungenVeraltet)
|
||||||
|| $geraetezugangFehlt
|
|| $geraetezugangFehlt
|
||||||
@@ -2266,12 +2332,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 +2482,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. ')
|
||||||
@@ -2480,8 +2544,7 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
$nutzlast = json_encode([
|
$nutzlast = json_encode([
|
||||||
'code' => $lizenzcode,
|
'code' => $lizenzcode,
|
||||||
'installationId' => $installationID,
|
'installationId' => $installationID,
|
||||||
'issueDeviceToken' => ($this->ReadPropertyBoolean('PrognoseAktiv')
|
'issueDeviceToken' => $this->geraetezugangBenoetigt()
|
||||||
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv'))
|
|
||||||
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '',
|
&& $this->ReadAttributeString('PrognoseInstallationsToken') === '',
|
||||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||||
$curl = curl_init(self::LIZENZ_ENDPOINT);
|
$curl = curl_init(self::LIZENZ_ENDPOINT);
|
||||||
@@ -2651,6 +2714,25 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function geraetezugangBenoetigt(): bool
|
||||||
|
{
|
||||||
|
if ($this->ReadPropertyBoolean('PrognoseAktiv')
|
||||||
|
|| $this->ReadPropertyBoolean('StoerueberwachungAktiv')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$topologie = $this->leseAnlagentopologie();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $topologie['inverters'] !== []
|
||||||
|
|| $topologie['pvArrays'] !== []
|
||||||
|
|| $topologie['batteries'] !== [];
|
||||||
|
}
|
||||||
|
|
||||||
private function prognoseTelemetrieKonfiguriert(): bool
|
private function prognoseTelemetrieKonfiguriert(): bool
|
||||||
{
|
{
|
||||||
return $this->ReadPropertyBoolean('PrognoseAktiv')
|
return $this->ReadPropertyBoolean('PrognoseAktiv')
|
||||||
@@ -2675,38 +2757,14 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$jetzt = time();
|
return ManagerRegler::interpolierteFahrplanVorgabe(
|
||||||
$naechsterAbstand = PHP_INT_MAX;
|
$fahrplan['points'],
|
||||||
$vorgabe = null;
|
time()
|
||||||
foreach ($fahrplan['points'] as $punkt) {
|
);
|
||||||
if (!is_array($punkt)
|
|
||||||
|| !is_string($punkt['time'] ?? null)
|
|
||||||
|| (!is_int($punkt['gridTargetW'] ?? null)
|
|
||||||
&& !is_float($punkt['gridTargetW'] ?? null))
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$zeitpunkt = strtotime($punkt['time']);
|
|
||||||
if ($zeitpunkt === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$abstand = abs($zeitpunkt - $jetzt);
|
|
||||||
if ($abstand < $naechsterAbstand) {
|
|
||||||
$naechsterAbstand = $abstand;
|
|
||||||
$vorgabe = (float) $punkt['gridTargetW'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $naechsterAbstand <= 1800 ? $vorgabe : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function synchronisiereAnlagentopologie(): void
|
private function synchronisiereAnlagentopologie(): void
|
||||||
{
|
{
|
||||||
if (!$this->ReadPropertyBoolean('PrognoseAktiv')) {
|
|
||||||
$this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$topologie = $this->leseAnlagentopologie();
|
$topologie = $this->leseAnlagentopologie();
|
||||||
if ($topologie['inverters'] === []
|
if ($topologie['inverters'] === []
|
||||||
@@ -2722,7 +2780,12 @@ class Manager extends IPSModule implements ManagerSchnittstelle
|
|||||||
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
|
throw new RuntimeException('Der Geraetezugang fuer die Prognose fehlt.');
|
||||||
}
|
}
|
||||||
$this->sendeAnlagentopologie($topologie, $token);
|
$this->sendeAnlagentopologie($topologie, $token);
|
||||||
$this->setzeDiagnosewert('Prognosestatus', 'Verbunden');
|
$this->setzeDiagnosewert(
|
||||||
|
'Prognosestatus',
|
||||||
|
$this->ReadPropertyBoolean('PrognoseAktiv')
|
||||||
|
? 'Verbunden'
|
||||||
|
: 'Topologie synchronisiert'
|
||||||
|
);
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
$meldung = substr($fehler->getMessage(), 0, 500);
|
$meldung = substr($fehler->getMessage(), 0, 500);
|
||||||
$this->setzeDiagnosewert('Prognosestatus', 'Fehler: ' . $meldung);
|
$this->setzeDiagnosewert('Prognosestatus', 'Fehler: ' . $meldung);
|
||||||
|
|||||||
@@ -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 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -130,6 +130,20 @@
|
|||||||
"minimum": 0.1,
|
"minimum": 0.1,
|
||||||
"digits": 1
|
"digits": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "TemperaturUntergrenze",
|
||||||
|
"caption": "Untertemperaturgrenze",
|
||||||
|
"suffix": " °C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "NumberSpinner",
|
||||||
|
"name": "TemperaturObergrenze",
|
||||||
|
"caption": "Übertemperaturgrenze",
|
||||||
|
"suffix": " °C",
|
||||||
|
"digits": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "List",
|
"type": "List",
|
||||||
"name": "Zeitplan",
|
"name": "Zeitplan",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
$this->RegisterPropertyString('Zeitplan', '[]');
|
$this->RegisterPropertyString('Zeitplan', '[]');
|
||||||
$this->RegisterPropertyInteger('LastwechselSperrzeit', 5);
|
$this->RegisterPropertyInteger('LastwechselSperrzeit', 5);
|
||||||
$this->RegisterPropertyInteger('TemperaturMaxAlter', 30);
|
$this->RegisterPropertyInteger('TemperaturMaxAlter', 30);
|
||||||
|
$this->RegisterPropertyFloat('TemperaturUntergrenze', 0.0);
|
||||||
|
$this->RegisterPropertyFloat('TemperaturObergrenze', 100.0);
|
||||||
$this->RegisterPropertyFloat('Hysterese', 5.0);
|
$this->RegisterPropertyFloat('Hysterese', 5.0);
|
||||||
$this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true);
|
$this->RegisterPropertyBoolean('LegionellenfunktionAktiv', true);
|
||||||
$this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4);
|
$this->RegisterPropertyInteger('LegionellenMinimalintervallTage', 4);
|
||||||
@@ -537,6 +539,10 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
if (!$temperaturGueltig) {
|
if (!$temperaturGueltig) {
|
||||||
$stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.';
|
$stoerungen[] = 'Boilertemperatur fehlt oder ist veraltet.';
|
||||||
}
|
}
|
||||||
|
$untertemperatur = $this->istUntertemperatur($temperaturGueltig);
|
||||||
|
if ($untertemperatur) {
|
||||||
|
$stoerungen[] = 'Untertemperatur erkannt.';
|
||||||
|
}
|
||||||
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
|
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
|
||||||
if ($uebertemperatur) {
|
if ($uebertemperatur) {
|
||||||
$stoerungen[] = 'Uebertemperatur erkannt.';
|
$stoerungen[] = 'Uebertemperatur erkannt.';
|
||||||
@@ -555,19 +561,18 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function istUntertemperatur(bool $temperaturGueltig): bool
|
||||||
|
{
|
||||||
|
return $temperaturGueltig
|
||||||
|
&& (float) $this->GetValue('Boilertemperatur')
|
||||||
|
< $this->ReadPropertyFloat('TemperaturUntergrenze');
|
||||||
|
}
|
||||||
|
|
||||||
private function istUebertemperatur(bool $temperaturGueltig): bool
|
private function istUebertemperatur(bool $temperaturGueltig): bool
|
||||||
{
|
{
|
||||||
if (!$temperaturGueltig) {
|
return $temperaturGueltig
|
||||||
return false;
|
&& (float) $this->GetValue('Boilertemperatur')
|
||||||
}
|
> $this->ReadPropertyFloat('TemperaturObergrenze');
|
||||||
|
|
||||||
$grenze = (float) $this->GetValue('Maximaltemperatur');
|
|
||||||
if ($this->ReadPropertyBoolean('LegionellenfunktionAktiv')) {
|
|
||||||
$grenze = max($grenze, $this->ReadAttributeFloat('ZustandLegionellentemperatur'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (float) $this->GetValue('Boilertemperatur')
|
|
||||||
> $grenze + $this->ReadPropertyFloat('Hysterese');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendeVerbraucherdaten(): void
|
private function sendeVerbraucherdaten(): void
|
||||||
@@ -592,6 +597,7 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
{
|
{
|
||||||
$temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig');
|
$temperaturGueltig = (bool) $this->leseZustand('TemperaturGueltig');
|
||||||
$schaltfehler = $this->ReadAttributeString('Schaltfehler');
|
$schaltfehler = $this->ReadAttributeString('Schaltfehler');
|
||||||
|
$untertemperatur = $this->istUntertemperatur($temperaturGueltig);
|
||||||
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
|
$uebertemperatur = $this->istUebertemperatur($temperaturGueltig);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -655,6 +661,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
'Einheit' => '',
|
'Einheit' => '',
|
||||||
'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.',
|
'Text' => $temperaturGueltig ? '' : 'Boilertemperatur fehlt oder ist veraltet.',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'Kennung' => 'Untertemperatur',
|
||||||
|
'Art' => 'Stoerung',
|
||||||
|
'Wert' => $untertemperatur,
|
||||||
|
'Einheit' => '',
|
||||||
|
'Text' => $untertemperatur ? 'Untertemperatur erkannt.' : '',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'Kennung' => 'Uebertemperatur',
|
'Kennung' => 'Uebertemperatur',
|
||||||
'Art' => 'Stoerung',
|
'Art' => 'Stoerung',
|
||||||
@@ -745,6 +758,13 @@ class VerbraucherWarmwassererwaermer extends IPSModule implements VerbraucherSch
|
|||||||
if ($this->ReadPropertyFloat('Hysterese') <= 0) {
|
if ($this->ReadPropertyFloat('Hysterese') <= 0) {
|
||||||
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
throw new InvalidArgumentException('Hysterese muss groesser als 0 sein.');
|
||||||
}
|
}
|
||||||
|
if ($this->ReadPropertyFloat('TemperaturUntergrenze')
|
||||||
|
>= $this->ReadPropertyFloat('TemperaturObergrenze')
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'Die Temperaturuntergrenze muss kleiner als die Temperaturobergrenze sein.'
|
||||||
|
);
|
||||||
|
}
|
||||||
$minimum = (float) $this->GetValue('Mindesttemperatur');
|
$minimum = (float) $this->GetValue('Mindesttemperatur');
|
||||||
$maximum = (float) $this->GetValue('Maximaltemperatur');
|
$maximum = (float) $this->GetValue('Maximaltemperatur');
|
||||||
$legionellen = (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur');
|
$legionellen = (float) $this->ReadAttributeFloat('ZustandLegionellentemperatur');
|
||||||
|
|||||||
+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`.
|
||||||
|
|||||||
@@ -13,9 +13,11 @@
|
|||||||
| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. |
|
| `Interval` | entfaellt | Die Regelung wird ereignisbasiert ausgeloest. |
|
||||||
| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. |
|
| `IdleCounterMax` | `LastwechselSperrzeit` | Zykluszaehler wird durch eine Sperrzeit in Sekunden ersetzt; Standard `5`. |
|
||||||
|
|
||||||
Die neuen Properties `TemperaturMaxAlter`, `Hysterese`, die Parameter der
|
Die neuen Properties `TemperaturMaxAlter`, `TemperaturUntergrenze`,
|
||||||
Legionellenfunktion und `DiagnosevariablenAnzeigen` ersetzen feste Werte und
|
`TemperaturObergrenze`, `Hysterese`, die Parameter der Legionellenfunktion und
|
||||||
nicht pruefbare Annahmen des Altmoduls.
|
`DiagnosevariablenAnzeigen` ersetzen feste Werte und nicht pruefbare Annahmen
|
||||||
|
des Altmoduls. Die Stoergrenzen liegen standardmaessig bei 0 und 100 Grad C und
|
||||||
|
sind von den Regel-Sollwerten `Mindesttemperatur` und `Maximaltemperatur` getrennt.
|
||||||
|
|
||||||
## Variablen
|
## Variablen
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,20 @@ Die Auswertung folgt bewusst dem Verhalten der bisherigen Enelix-1-Ladestation:
|
|||||||
wird entsprechend `State != 1` ausgewertet.
|
wird entsprechend `State != 1` ausgewertet.
|
||||||
2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei
|
2. Die gemessene Leistung wird bei der alten go-e-API mit Faktor 10, bei
|
||||||
Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet.
|
Gemini direkt in Watt und bei Pico von kW in Watt umgerechnet.
|
||||||
3. Bei einer Leistung ueber 7500 W wird dreiphasiges Laden erkannt. Darunter
|
3. go-e meldet die drei Phasenstroeme einzeln. Sobald mindestens zwei Phasen
|
||||||
wird bei verbundenem Fahrzeug einphasiges Laden angenommen.
|
Strom fuehren, wird dreiphasiges Laden direkt erkannt.
|
||||||
4. Der Ladestrom wird aus der Leistung mit `230 V` einphasig beziehungsweise
|
4. Sind beim Anstecken noch keine belastbaren Phasenwerte vorhanden, gibt das
|
||||||
`1.71 * 400 V` dreiphasig berechnet.
|
Modul zunaechst den konfigurierten Maximalstrom als Erkennungsstrom vor. Bis
|
||||||
5. War das Fahrzeug bereits im vorherigen Zyklus verbunden und liegt der
|
zur abgeschlossenen Erkennung bleibt `Phasenzahl=0` und das EMS-Angebot
|
||||||
ermittelte Maximalstrom aus Ladestrom plus `2.5 A` unter `6 A`, wird
|
`[0]`.
|
||||||
`FahrzeugGeladen=true` gesetzt.
|
5. Nach `Phasenerkennungszeit` gilt wie in Enelix 1: ueber 7500 W werden drei
|
||||||
|
Phasen erkannt, andernfalls eine Phase. Die erkannte Phasenzahl bleibt bis
|
||||||
|
zum Ausstecken erhalten und kippt waehrend einer Ladepause nicht zurueck.
|
||||||
|
6. Der Ladestrom wird aus der Leistung mit `230 W/A` einphasig beziehungsweise
|
||||||
|
`684 W/A` dreiphasig berechnet.
|
||||||
|
7. Ein Ladeende wird nur nach zuvor tatsaechlich gemessener Ladeleistung
|
||||||
|
erkannt. Eine vom EMS angeordnete Solarpause gilt deshalb nicht als
|
||||||
|
`FahrzeugGeladen`.
|
||||||
|
|
||||||
Der normalisierte `Fahrzeugstatus` verwendet folgende Werte:
|
Der normalisierte `Fahrzeugstatus` verwendet folgende Werte:
|
||||||
|
|
||||||
@@ -72,10 +79,18 @@ stoppt die Ladestation.
|
|||||||
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
|
| Peak | ein | ausschliesslich `[0]`; die Ladestation wird gestoppt |
|
||||||
| Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
| Peak | aus | `0 W` und alle Ladestufen von 6 A bis zum konfigurierten Maximum |
|
||||||
|
|
||||||
Die festen ein- und dreiphasigen Leistungsstufen sowie deren Rundung wurden aus
|
Die Leistungsstufen entsprechen Enelix 1: `230 W` pro Ampere einphasig und
|
||||||
Enelix 1 uebernommen. Ohne gueltige Manager-Vorgabe faehrt das Modul bei
|
`684 W` pro Ampere dreiphasig. Ohne gueltige Manager-Vorgabe faehrt das Modul
|
||||||
aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es die
|
bei aktivem Solarladen mit `0 A`; bei ausgeschaltetem Solarladen verwendet es
|
||||||
maximale angebotene Leistung.
|
die maximale angebotene Leistung.
|
||||||
|
|
||||||
|
Nach einer Leistungsaenderung wird die aktuelle Stufe fuer
|
||||||
|
`ZeitZwischenZustandswechseln` gehalten. Waehrend der
|
||||||
|
`Mindesteinschaltdauer` bleiben positive Leistungsstufen regelbar, `0 W`
|
||||||
|
wird jedoch nicht angeboten. Nach dem Abschalten meldet das Modul waehrend der
|
||||||
|
`Mindestausschaltdauer` ausschliesslich `[0]`. Sicherheitsabschaltungen
|
||||||
|
durch Deaktivierung, fehlende Ladefreigabe, Ausstecken oder ein erkanntes
|
||||||
|
Ladeende greifen sofort.
|
||||||
|
|
||||||
## Bedienung
|
## Bedienung
|
||||||
|
|
||||||
@@ -114,8 +129,13 @@ gueltige Manager-Vorgabe und berechnet das Leistungsangebot sofort neu.
|
|||||||
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
| `Benutzername` | String | leer | Benutzername fuer die Pico-API. |
|
||||||
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
| `Passwort` | String | leer | Passwort fuer die Pico-API; im Formular als Passwortfeld dargestellt. |
|
||||||
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
| `MaximalerLadestrom` | Integer | `16 A` | Obergrenze der angebotenen Ladestufen; zulaessig sind 6 bis 32 A. |
|
||||||
|
| `Phasenerkennungszeit` | Integer | `60 s` | Dauer der Maximalstrom-Probe vor der leistungsbasierten Phasenerkennung. |
|
||||||
|
| `FahrzeugstromErkennungszeit` | Integer | `90 s` | Zeit, die eine stabile Stromunterschreitung anliegen muss, bevor die Fahrzeuggrenze mit 2,5 A Reserve uebernommen wird. |
|
||||||
|
| `ZeitZwischenZustandswechseln` | Integer | `1 min` | Mindestabstand zwischen zwei Leistungsaenderungen. |
|
||||||
|
| `Mindesteinschaltdauer` | Integer | `0 min` | Mindestdauer nach dem Einschalten, in der kein Abschalten auf 0 W angeboten wird. |
|
||||||
|
| `Mindestausschaltdauer` | Integer | `0 min` | Mindestpause nach dem Abschalten, in der nur 0 W angeboten werden. |
|
||||||
| `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. |
|
| `Abfrageintervall` | Integer | `5 s` | Intervall der Geraetestatusabfrage. |
|
||||||
| `Ladefreigabe` | Boolean | `false` | Startwert der lokalen Ladefreigabe. |
|
| `Ladefreigabe` | Boolean | `true` | Startwert der lokalen Ladefreigabe. |
|
||||||
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
|
| `Solarladen` | Boolean | `true` | Startwert der lokalen Solarlogik. |
|
||||||
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. |
|
| `DiagnosevariablenAnzeigen` | Boolean | `false` | Blendet gemeinsame und ladestationsspezifische Diagnosevariablen ein. |
|
||||||
|
|
||||||
@@ -153,7 +173,7 @@ Nur mit `DiagnosevariablenAnzeigen=true` sichtbar:
|
|||||||
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
|
| `AenderungMoeglich` | Zeigt, ob das aktuelle Angebot mehr als eine Leistungsstufe enthaelt. |
|
||||||
| `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. |
|
| `Stoerung` | Sammelstatus fuer Konfigurations- und Kommunikationsfehler. |
|
||||||
| `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. |
|
| `Stoertext` | Letzte verstaendliche Fehlerbeschreibung. |
|
||||||
| `LetzterGeraetebefehl` | Letzte HTTP-Methode und URL ohne Zugangsdaten. |
|
| `LetzterGeraetebefehl` | Letzter Steuerbefehl mit HTTP-Methode und URL ohne Zugangsdaten; Statusabfragen ueberschreiben ihn nicht. |
|
||||||
| `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. |
|
| `LeistungsangebotDiagnose` | Aktuelles Leistungsangebot als JSON-Liste in W. |
|
||||||
|
|
||||||
Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der
|
Die Regellogik speichert ihren Zustand unabhaengig von der Sichtbarkeit der
|
||||||
@@ -232,16 +252,22 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
|
|||||||
- Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`.
|
- Die Instanz erreicht Status `102`, und `Stoerung` bleibt `false`.
|
||||||
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
- Ohne Fahrzeug sind `FahrzeugVerbunden=false`, `Phasenzahl=0` und das
|
||||||
Leistungsangebot `[0]`.
|
Leistungsangebot `[0]`.
|
||||||
- Nach dem Anstecken wird das Fahrzeug erkannt und die gemessene Leistung
|
- Nach dem Anstecken wird das Fahrzeug erkannt und bei unbekannten Phasen
|
||||||
plausibel in `Istleistung` und `Ladestrom` abgebildet.
|
zunaechst der konfigurierte Maximalstrom zur Erkennung angefordert.
|
||||||
- Bei einer Ladeleistung bis 7500 W wird eine Phase, darueber werden drei
|
- Nach der Erkennungszeit werden bis 7500 W eine Phase, darueber drei Phasen
|
||||||
Phasen angezeigt.
|
gespeichert. Bei go-e koennen die einzelnen Phasenstroeme die Erkennung
|
||||||
|
bereits vorher abschliessen.
|
||||||
|
- Die erkannte Phasenzahl bleibt bei einer anschliessenden Ladepause stabil.
|
||||||
|
- Die gemessene Leistung wird plausibel in `Istleistung` und `Ladestrom`
|
||||||
|
abgebildet.
|
||||||
- `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation.
|
- `Aktiv=false` oder `Ladefreigabe=false` stoppt die Ladestation.
|
||||||
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an.
|
- PV mit `Solarladen=true` bietet `0 W` und die Ladestufen an.
|
||||||
- PV mit `Solarladen=false` bietet nur die maximale Leistung an.
|
- PV mit `Solarladen=false` bietet nur die maximale Leistung an.
|
||||||
- Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an.
|
- Peak mit `Solarladen=true` stoppt die Ladestation und bietet nur `[0]` an.
|
||||||
- Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an.
|
- Peak mit `Solarladen=false` bietet `0 W` und die Ladestufen an.
|
||||||
- Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen.
|
- Eine Manager-Vorgabe ausserhalb des gemeldeten Angebots wird abgewiesen.
|
||||||
|
- Leistungswechsel-, Mindest-Ein- und Mindest-Aus-Zeiten werden im gemeldeten
|
||||||
|
Angebot sichtbar und vom Manager eingehalten.
|
||||||
- Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten.
|
- Nach Ablauf des Vorgabe-Timeouts gilt wieder das lokale Ersatzverhalten.
|
||||||
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
|
- Im Debug-Protokoll und in `LetzterGeraetebefehl` erscheinen keine
|
||||||
Zugangsdaten.
|
Zugangsdaten.
|
||||||
@@ -256,9 +282,11 @@ niemals in Repository-Dateien, Skripte oder Screenshots.
|
|||||||
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
|
- Fahrzeug wird nicht erkannt: Rohstatus der Geraete-API kontrollieren. Der
|
||||||
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
|
Adapter erwartet bei go-e `car` und `nrg[11]`, bei Pico `State` und
|
||||||
`ActiveChargingPower`.
|
`ActiveChargingPower`.
|
||||||
- Falsche Phasenzahl bei sehr kleiner oder stehender Ladung: Die kompatible
|
- Phasenzahl bleibt laenger auf `0`: Waehrend der Erkennungsphase muessen
|
||||||
Enelix-1-Erkennung basiert auf der momentanen Leistung. Fuer eine sichere
|
Ladestation und Fahrzeug den angeforderten Maximalstrom tatsaechlich
|
||||||
Dreiphasenerkennung muss waehrend der Erkennung mehr als 7500 W anliegen.
|
freigeben. Bei Pico muss fuer eine sichere Dreiphasenerkennung waehrend der
|
||||||
|
Erkennung mehr als 7500 W anliegen; go-e kann zusaetzlich anhand der
|
||||||
|
einzelnen Phasenstroeme erkennen.
|
||||||
- Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie
|
- Manager-Vorgabe wird abgewiesen: aktive Zuordnung im Manager sowie
|
||||||
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
|
`LeistungsangebotDiagnose` und die Betriebsart kontrollieren.
|
||||||
|
|
||||||
@@ -270,9 +298,10 @@ URL, HTTP-Methode, Authentisierung und Steueraufrufe ohne reale Hardware
|
|||||||
geprueft.
|
geprueft.
|
||||||
|
|
||||||
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
Der Symcon-Funktionstest verwendet den internen `Testmodus` mit simulierten
|
||||||
API-Antworten. Er prueft alle drei Geraetevarianten, Fahrzeug- und
|
API-Antworten. Er prueft alle drei Geraetevarianten, die Maximalstrom-Probe
|
||||||
Phasenerkennung, die Leistungsangebote in PV und Peak sowie die Umschaltung
|
beim Anstecken, stabile Phasen waehrend einer Ladepause, die Schaltsperren,
|
||||||
ueber den `Solarladen`-Button.
|
Fahrzeugstrom-Erkennung, die Leistungsangebote in PV und Peak sowie die
|
||||||
|
Umschaltung ueber den `Solarladen`-Button.
|
||||||
|
|
||||||
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
|
PHP-Syntax, Struktur- und Unit-Tests des gesamten Repositorys:
|
||||||
|
|
||||||
|
|||||||
@@ -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,22 @@ 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.
|
sortiert. Bei gleicher Priorität bevorzugt der Manager wie Enelix 1 den
|
||||||
|
Verbraucher mit der bisher kleineren bezogenen Energie; verglichen wird in
|
||||||
|
2-kWh-Gruppen. Erst wenn auch diese Gruppe gleich ist, entscheidet stabil die
|
||||||
|
Instanz-ID. 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
|
||||||
* }
|
* }
|
||||||
@@ -48,13 +53,18 @@ final class EaseeLadestatus
|
|||||||
? self::phasenzahl((int) ($beobachtungen['110'] ?? 0))
|
? self::phasenzahl((int) ($beobachtungen['110'] ?? 0))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
$leistung = max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0);
|
$laedt = $betriebszustand === 3;
|
||||||
|
$leistung = $laedt
|
||||||
|
? max(0.0, (float) ($beobachtungen['120'] ?? 0.0) * 1000.0)
|
||||||
|
: 0.0;
|
||||||
$stroeme = [];
|
$stroeme = [];
|
||||||
|
if ($laedt) {
|
||||||
foreach ([182, 183, 184, 185] as $id) {
|
foreach ([182, 183, 184, 185] as $id) {
|
||||||
if (array_key_exists((string) $id, $beobachtungen)) {
|
if (array_key_exists((string) $id, $beobachtungen)) {
|
||||||
$stroeme[] = abs((float) $beobachtungen[(string) $id]);
|
$stroeme[] = abs((float) $beobachtungen[(string) $id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$ladestrom = $stroeme === [] ? 0.0 : max($stroeme);
|
$ladestrom = $stroeme === [] ? 0.0 : max($stroeme);
|
||||||
|
|
||||||
$grenzen = [$konfigurierterMaximalstrom];
|
$grenzen = [$konfigurierterMaximalstrom];
|
||||||
@@ -65,6 +75,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 +105,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),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ final class LadestationAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{Rohstatus: int, Istleistung_W: float}
|
* @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int}
|
||||||
*/
|
*/
|
||||||
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
||||||
{
|
{
|
||||||
@@ -110,12 +110,33 @@ final class LadestationAdapter
|
|||||||
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
|
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$faktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
|
$leistungsfaktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
|
||||||
|
$status = [
|
||||||
return [
|
|
||||||
'Rohstatus' => (int) $daten['car'],
|
'Rohstatus' => (int) $daten['car'],
|
||||||
'Istleistung_W' => (float) $daten['nrg'][11] * $faktor,
|
'Istleistung_W' => (float) $daten['nrg'][11] * $leistungsfaktor,
|
||||||
];
|
];
|
||||||
|
if (isset($daten['nrg'][4], $daten['nrg'][5], $daten['nrg'][6])
|
||||||
|
&& is_numeric($daten['nrg'][4])
|
||||||
|
&& is_numeric($daten['nrg'][5])
|
||||||
|
&& is_numeric($daten['nrg'][6])
|
||||||
|
) {
|
||||||
|
$stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0;
|
||||||
|
$phasenstroeme = [
|
||||||
|
(float) $daten['nrg'][4] * $stromfaktor,
|
||||||
|
(float) $daten['nrg'][5] * $stromfaktor,
|
||||||
|
(float) $daten['nrg'][6] * $stromfaktor,
|
||||||
|
];
|
||||||
|
$status['Iststrom_A'] = max($phasenstroeme);
|
||||||
|
$aktivePhasen = count(array_filter(
|
||||||
|
$phasenstroeme,
|
||||||
|
static fn (float $strom): bool => $strom >= 1.0
|
||||||
|
));
|
||||||
|
if ($aktivePhasen > 0) {
|
||||||
|
$status['Phasenzahl'] = $aktivePhasen >= 2 ? 3 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($geraetetyp === self::SMART_ME_PICO) {
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
||||||
|
|||||||
+72
-29
@@ -12,30 +12,25 @@ final class LadestationRegler
|
|||||||
public const PHASEN_EIN = 1;
|
public const PHASEN_EIN = 1;
|
||||||
public const PHASEN_DREI = 3;
|
public const PHASEN_DREI = 3;
|
||||||
|
|
||||||
/** @var list<int> */
|
private const LEISTUNG_PRO_AMPERE_EINPHASIG = 230;
|
||||||
private const LEISTUNGEN_EINPHASIG = [
|
private const LEISTUNG_PRO_AMPERE_DREIPHASIG = 684;
|
||||||
1450, 1690, 1930, 2170, 2410, 2650, 2890, 3130, 3370,
|
|
||||||
3610, 3850, 4090, 4330, 4570, 4810, 5050, 5290, 5530,
|
|
||||||
5770, 6010, 6250, 6490, 6730, 6970, 7210, 7450, 7690,
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var list<int> */
|
|
||||||
private const LEISTUNGEN_DREIPHASIG = [
|
|
||||||
4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700,
|
|
||||||
10300, 11000, 11750, 12450, 13150, 13850, 14550, 15250,
|
|
||||||
15900, 16600, 17300, 18000, 18700, 19400, 20100, 20800,
|
|
||||||
21500, 22000,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Schwelle, die Voll-Erkennung und der Zuschlag von 2.5 A stammen
|
* Die Schwelle und der Zuschlag von 2.5 A stammen aus der Enelix-1-Ladestation.
|
||||||
* aus der Enelix-1-Ladestation und bleiben bewusst kompatibel.
|
* Ein Ladeende wird erst nach zuvor gemessener Ladeleistung angenommen, damit
|
||||||
|
* die Anlaufzeit nach einer neuen Stromvorgabe nicht als volles Fahrzeug gilt.
|
||||||
*
|
*
|
||||||
* @param array{Rohstatus: int, Istleistung_W: float} $rohstatus
|
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $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,
|
||||||
|
bool $ladeleistungWurdeErkannt = false,
|
||||||
|
int $bekanntePhasenzahl = self::PHASEN_UNBEKANNT
|
||||||
|
): 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;
|
||||||
@@ -43,11 +38,28 @@ final class LadestationRegler
|
|||||||
$geladen = false;
|
$geladen = false;
|
||||||
|
|
||||||
if ($verbunden) {
|
if ($verbunden) {
|
||||||
$phasen = $leistung > 7500.0 ? self::PHASEN_DREI : self::PHASEN_EIN;
|
if (in_array($bekanntePhasenzahl, [self::PHASEN_EIN, self::PHASEN_DREI], true)) {
|
||||||
$nenner = $phasen === self::PHASEN_EIN ? 230.0 : 1.71 * 400.0;
|
$phasen = $bekanntePhasenzahl;
|
||||||
$ladestrom = $leistung / $nenner;
|
} elseif (isset($rohstatus['Phasenzahl'])
|
||||||
|
&& in_array($rohstatus['Phasenzahl'], [self::PHASEN_EIN, self::PHASEN_DREI], true)
|
||||||
|
) {
|
||||||
|
$phasen = (int) $rohstatus['Phasenzahl'];
|
||||||
|
} elseif ($leistung > 7500.0) {
|
||||||
|
$phasen = self::PHASEN_DREI;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nenner = $phasen === self::PHASEN_DREI
|
||||||
|
? self::LEISTUNG_PRO_AMPERE_DREIPHASIG
|
||||||
|
: self::LEISTUNG_PRO_AMPERE_EINPHASIG;
|
||||||
|
$ladestrom = isset($rohstatus['Iststrom_A'])
|
||||||
|
? max(0.0, (float) $rohstatus['Iststrom_A'])
|
||||||
|
: ($phasen === self::PHASEN_UNBEKANNT ? 0.0 : $leistung / $nenner);
|
||||||
$ermittelterMaximalstrom = 2.5 + $ladestrom;
|
$ermittelterMaximalstrom = 2.5 + $ladestrom;
|
||||||
$geladen = $warVerbunden && $ermittelterMaximalstrom < 6.0;
|
$geladen = $warGeladen
|
||||||
|
|| ($warVerbunden
|
||||||
|
&& $ladungErwartet
|
||||||
|
&& $ladeleistungWurdeErkannt
|
||||||
|
&& $ermittelterMaximalstrom < 6.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$verbunden) {
|
if (!$verbunden) {
|
||||||
@@ -70,6 +82,29 @@ final class LadestationRegler
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ermittleFahrzeugMaximalstrom(
|
||||||
|
int $vorgegebenerStrom,
|
||||||
|
float $gemessenerStrom,
|
||||||
|
int $konfigurierterMaximalstrom
|
||||||
|
): ?int {
|
||||||
|
if ($konfigurierterMaximalstrom < 6 || $konfigurierterMaximalstrom > 32) {
|
||||||
|
throw new InvalidArgumentException('MaximalerLadestrom muss zwischen 6 und 32 A liegen.');
|
||||||
|
}
|
||||||
|
if ($vorgegebenerStrom < 6 || $gemessenerStrom < 5.5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$kandidat = max(
|
||||||
|
6,
|
||||||
|
min($konfigurierterMaximalstrom, (int) floor($gemessenerStrom + 2.5))
|
||||||
|
);
|
||||||
|
if ($kandidat > $vorgegebenerStrom - 2 || $kandidat >= $konfigurierterMaximalstrom) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $kandidat;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return list<int> */
|
/** @return list<int> */
|
||||||
public static function leistungsangebot(
|
public static function leistungsangebot(
|
||||||
bool $aktiv,
|
bool $aktiv,
|
||||||
@@ -85,6 +120,10 @@ final class LadestationRegler
|
|||||||
return [0];
|
return [0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($phasenzahl === self::PHASEN_UNBEKANNT) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
|
$stufen = self::leistungsstufen($phasenzahl, $maximalerLadestrom);
|
||||||
if ($stufen === []) {
|
if ($stufen === []) {
|
||||||
return [0];
|
return [0];
|
||||||
@@ -135,12 +174,16 @@ final class LadestationRegler
|
|||||||
private static function stufenFuerPhasen(int $phasenzahl): array
|
private static function stufenFuerPhasen(int $phasenzahl): array
|
||||||
{
|
{
|
||||||
if ($phasenzahl === self::PHASEN_EIN) {
|
if ($phasenzahl === self::PHASEN_EIN) {
|
||||||
return self::LEISTUNGEN_EINPHASIG;
|
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_EINPHASIG;
|
||||||
}
|
} elseif ($phasenzahl === self::PHASEN_DREI) {
|
||||||
if ($phasenzahl === self::PHASEN_DREI) {
|
$leistungProAmpere = self::LEISTUNG_PRO_AMPERE_DREIPHASIG;
|
||||||
return self::LEISTUNGEN_DREIPHASIG;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
|
throw new InvalidArgumentException('Phasenzahl muss 1 oder 3 sein.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
static fn (int $strom): int => $strom * $leistungProAmpere,
|
||||||
|
range(6, 32)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -256,12 +256,73 @@ trait ManagerEnergieTrait
|
|||||||
IPS_SetName($instanzID, 'Energiefluss');
|
IPS_SetName($instanzID, 'Energiefluss');
|
||||||
IPS_SetPosition($instanzID, 540);
|
IPS_SetPosition($instanzID, 540);
|
||||||
IPS_SetHidden($instanzID, false);
|
IPS_SetHidden($instanzID, false);
|
||||||
|
|
||||||
|
$profil = $this->energieflussLeistungsprofil();
|
||||||
|
$pvID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussPV',
|
||||||
|
'Energiefluss PV',
|
||||||
|
$profil,
|
||||||
|
600
|
||||||
|
);
|
||||||
|
$netzID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussNetz',
|
||||||
|
'Energiefluss Netz',
|
||||||
|
$profil,
|
||||||
|
610
|
||||||
|
);
|
||||||
|
$hausID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussHaus',
|
||||||
|
'Energiefluss Hausverbrauch',
|
||||||
|
$profil,
|
||||||
|
620
|
||||||
|
);
|
||||||
|
$batterieID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussBatterie',
|
||||||
|
'Energiefluss Batteriespeicher',
|
||||||
|
$profil,
|
||||||
|
630
|
||||||
|
);
|
||||||
|
|
||||||
|
$verbraucher = $this->energieflussVerbraucher();
|
||||||
$knoten = [
|
$knoten = [
|
||||||
$this->energieflussKnoten('PVLeistungArchiv', 'PV-Anlage', 'solar'),
|
$this->energieflussKnoten($pvID, 'PV-Anlage', 'solar'),
|
||||||
$this->energieflussKnoten('NetzleistungArchiv', 'Stromnetz', 'grid'),
|
$this->energieflussKnoten($netzID, 'Stromnetz', 'grid'),
|
||||||
$this->energieflussKnoten('HausverbrauchLeistungArchiv', 'Hausverbrauch', 'consumer'),
|
$this->energieflussKnoten($hausID, 'Hausverbrauch', 'consumer'),
|
||||||
$this->energieflussKnoten('BatterieleistungArchiv', 'Batteriespeicher', 'battery'),
|
|
||||||
];
|
];
|
||||||
|
$hatBatterieverbraucher = false;
|
||||||
|
$position = 640;
|
||||||
|
foreach ($verbraucher as $eintrag) {
|
||||||
|
$istID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussIst_' . $eintrag['InstanzID'],
|
||||||
|
$eintrag['Name'] . ' Istleistung',
|
||||||
|
$profil,
|
||||||
|
$position
|
||||||
|
);
|
||||||
|
$sollID = $this->energieflussLeistungsvariable(
|
||||||
|
'EnergieflussSoll_' . $eintrag['InstanzID'],
|
||||||
|
$eintrag['Name'] . ' Sollleistung',
|
||||||
|
$profil,
|
||||||
|
$position + 1
|
||||||
|
);
|
||||||
|
$knoten[] = $this->energieflussKnoten(
|
||||||
|
$istID,
|
||||||
|
$eintrag['Name'],
|
||||||
|
$eintrag['Typ'],
|
||||||
|
true,
|
||||||
|
$sollID
|
||||||
|
);
|
||||||
|
$hatBatterieverbraucher = $hatBatterieverbraucher
|
||||||
|
|| $eintrag['Typ'] === 'storage';
|
||||||
|
$position += 10;
|
||||||
|
}
|
||||||
|
if (!$hatBatterieverbraucher) {
|
||||||
|
$knoten[] = $this->energieflussKnoten(
|
||||||
|
$batterieID,
|
||||||
|
'Batteriespeicher',
|
||||||
|
'storage'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
IPS_SetProperty($instanzID, 'Calculation', true);
|
IPS_SetProperty($instanzID, 'Calculation', true);
|
||||||
IPS_SetProperty(
|
IPS_SetProperty(
|
||||||
$instanzID,
|
$instanzID,
|
||||||
@@ -271,26 +332,166 @@ trait ManagerEnergieTrait
|
|||||||
IPS_SetProperty($instanzID, 'EnableLimits', false);
|
IPS_SetProperty($instanzID, 'EnableLimits', false);
|
||||||
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
IPS_SetProperty($instanzID, 'Restrictions', '[]');
|
||||||
IPS_ApplyChanges($instanzID);
|
IPS_ApplyChanges($instanzID);
|
||||||
|
$this->aktualisiereEnergieflusswerte($verbraucher);
|
||||||
|
|
||||||
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
return 'Der Energiefluss wurde erstellt oder aktualisiert.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function energieflussKnoten(string $ident, string $name, string $typ): array
|
private function energieflussKnoten(
|
||||||
{
|
int $variableID,
|
||||||
|
string $name,
|
||||||
|
string $typ,
|
||||||
|
bool $gruppiert = false,
|
||||||
|
int $infoVariableID = 1
|
||||||
|
): array {
|
||||||
return [
|
return [
|
||||||
'VariableConfigType' => 0,
|
'VariableConfigType' => 0,
|
||||||
'VariableID' => $this->GetIDForIdent($ident),
|
'VariableID' => $variableID,
|
||||||
'SecondaryVariableID' => 1,
|
'SecondaryVariableID' => 1,
|
||||||
'Name' => $name,
|
'Name' => $name,
|
||||||
'Type' => $typ,
|
'Type' => $typ,
|
||||||
'Grouped' => false,
|
'Grouped' => $gruppiert,
|
||||||
'CalculationType' => 0,
|
'CalculationType' => 0,
|
||||||
'InfoVariableID' => 1,
|
'InfoVariableID' => $infoVariableID,
|
||||||
'Invert' => false,
|
'Invert' => false,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function energieflussLeistungsprofil(): string
|
||||||
|
{
|
||||||
|
if ($this->ReadPropertyInteger('EnergieflussLeistungseinheit') !== 1) {
|
||||||
|
return '~Power';
|
||||||
|
}
|
||||||
|
|
||||||
|
$profil = 'Enelix.Power.kW';
|
||||||
|
if (!IPS_VariableProfileExists($profil)) {
|
||||||
|
IPS_CreateVariableProfile($profil, 2);
|
||||||
|
IPS_SetVariableProfileValues($profil, -1000000.0, 1000000.0, 0.001);
|
||||||
|
IPS_SetVariableProfileDigits($profil, 3);
|
||||||
|
IPS_SetVariableProfileText($profil, '', ' kW');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profil;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function energieflussLeistungsvariable(
|
||||||
|
string $ident,
|
||||||
|
string $name,
|
||||||
|
string $profil,
|
||||||
|
int $position
|
||||||
|
): int {
|
||||||
|
$this->RegisterVariableFloat($ident, $name, $profil, $position);
|
||||||
|
$variableID = $this->GetIDForIdent($ident);
|
||||||
|
IPS_SetHidden($variableID, true);
|
||||||
|
|
||||||
|
return $variableID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* InstanzID: int,
|
||||||
|
* Name: string,
|
||||||
|
* Typ: string,
|
||||||
|
* Istleistung_W: float,
|
||||||
|
* Sollleistung_W: float
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private function energieflussVerbraucher(): array
|
||||||
|
{
|
||||||
|
$cache = $this->leseJsonAttribut('VerbraucherCache');
|
||||||
|
$ergebnis = [];
|
||||||
|
foreach ($this->konfigurierteAktiveVerbraucherIDs() as $instanzID) {
|
||||||
|
if (!IPS_InstanceExists($instanzID)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$daten = $cache[(string) $instanzID]['Daten'] ?? null;
|
||||||
|
$istleistung = 0.0;
|
||||||
|
$sollleistung = 0.0;
|
||||||
|
if (is_array($daten)) {
|
||||||
|
$istleistung = $this->verbraucherleistung($daten);
|
||||||
|
$sollleistung = (float) (ManagerRegler::angenommenerSollwert($daten) ?? 0);
|
||||||
|
}
|
||||||
|
$modulID = strtoupper(
|
||||||
|
(string) IPS_GetInstance($instanzID)['ModuleInfo']['ModuleID']
|
||||||
|
);
|
||||||
|
$katalog = self::VERBRAUCHER_LIZENZEN[$modulID]['Katalog'] ?? '';
|
||||||
|
|
||||||
|
$ergebnis[] = [
|
||||||
|
'InstanzID' => $instanzID,
|
||||||
|
'Name' => IPS_GetName($instanzID),
|
||||||
|
'Typ' => match ($katalog) {
|
||||||
|
'battery' => 'storage',
|
||||||
|
'ev_charger' => 'wallbox',
|
||||||
|
default => 'consumer',
|
||||||
|
},
|
||||||
|
'Istleistung_W' => $istleistung,
|
||||||
|
'Sollleistung_W' => $sollleistung,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>>|null $verbraucher
|
||||||
|
*/
|
||||||
|
private function aktualisiereEnergieflusswerte(?array $verbraucher = null): void
|
||||||
|
{
|
||||||
|
if (!$this->ReadPropertyBoolean('EnergieflussAnzeigen')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$verbraucher = $verbraucher ?? $this->energieflussVerbraucher();
|
||||||
|
$verbraucherleistung = 0.0;
|
||||||
|
$hatBatterieverbraucher = false;
|
||||||
|
foreach ($verbraucher as $eintrag) {
|
||||||
|
$this->setzeEnergieflussLeistungswert(
|
||||||
|
'EnergieflussIst_' . $eintrag['InstanzID'],
|
||||||
|
(float) $eintrag['Istleistung_W']
|
||||||
|
);
|
||||||
|
$this->setzeEnergieflussLeistungswert(
|
||||||
|
'EnergieflussSoll_' . $eintrag['InstanzID'],
|
||||||
|
(float) $eintrag['Sollleistung_W']
|
||||||
|
);
|
||||||
|
if ($eintrag['Typ'] === 'storage') {
|
||||||
|
$hatBatterieverbraucher = true;
|
||||||
|
} else {
|
||||||
|
$verbraucherleistung += max(0.0, (float) $eintrag['Istleistung_W']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$leistungen = $this->leseMessleistungen();
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->protokolliere('Energiefluss', $fehler->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->setzeEnergieflussLeistungswert('EnergieflussPV', $leistungen['PV']);
|
||||||
|
$this->setzeEnergieflussLeistungswert('EnergieflussNetz', $leistungen['Netz']);
|
||||||
|
$this->setzeEnergieflussLeistungswert(
|
||||||
|
'EnergieflussHaus',
|
||||||
|
max(0.0, $leistungen['Haus'] - $verbraucherleistung)
|
||||||
|
);
|
||||||
|
if (!$hatBatterieverbraucher) {
|
||||||
|
$this->setzeEnergieflussLeistungswert(
|
||||||
|
'EnergieflussBatterie',
|
||||||
|
$leistungen['Batterie']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setzeEnergieflussLeistungswert(string $ident, float $watt): void
|
||||||
|
{
|
||||||
|
$variableID = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
|
||||||
|
if (!is_int($variableID) || $variableID <= 0 || !IPS_VariableExists($variableID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$faktor = $this->ReadPropertyInteger('EnergieflussLeistungseinheit') === 1
|
||||||
|
? 0.001
|
||||||
|
: 1.0;
|
||||||
|
SetValue($variableID, $watt * $faktor);
|
||||||
|
}
|
||||||
|
|
||||||
private function erstelleEnergiediagramme(): string
|
private function erstelleEnergiediagramme(): string
|
||||||
{
|
{
|
||||||
$leistung = [
|
$leistung = [
|
||||||
|
|||||||
+211
-12
@@ -19,6 +19,71 @@ final class ManagerRegler
|
|||||||
public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV;
|
public const BETRIEBSART_PV = Nachrichtenvertrag::BETRIEBSART_PV;
|
||||||
public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK;
|
public const BETRIEBSART_PEAK = Nachrichtenvertrag::BETRIEBSART_PEAK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $punkte
|
||||||
|
*/
|
||||||
|
public static function interpolierteFahrplanVorgabe(
|
||||||
|
array $punkte,
|
||||||
|
int $jetzt,
|
||||||
|
int $maximalerPunktabstandSekunden = 1800
|
||||||
|
): ?float {
|
||||||
|
if ($maximalerPunktabstandSekunden < 0) {
|
||||||
|
throw new InvalidArgumentException('Maximaler Fahrplanpunktabstand darf nicht negativ sein.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$gueltigePunkte = [];
|
||||||
|
foreach ($punkte as $punkt) {
|
||||||
|
if (!is_array($punkt)
|
||||||
|
|| !is_string($punkt['time'] ?? null)
|
||||||
|
|| (!is_int($punkt['gridTargetW'] ?? null)
|
||||||
|
&& !is_float($punkt['gridTargetW'] ?? null))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$zeitpunkt = strtotime($punkt['time']);
|
||||||
|
$vorgabe = (float) $punkt['gridTargetW'];
|
||||||
|
if ($zeitpunkt === false || !is_finite($vorgabe)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$gueltigePunkte[$zeitpunkt] = $vorgabe;
|
||||||
|
}
|
||||||
|
if ($gueltigePunkte === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ksort($gueltigePunkte, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$vorherigerZeitpunkt = null;
|
||||||
|
$vorherigeVorgabe = null;
|
||||||
|
foreach ($gueltigePunkte as $zeitpunkt => $vorgabe) {
|
||||||
|
if ($zeitpunkt === $jetzt) {
|
||||||
|
return $vorgabe;
|
||||||
|
}
|
||||||
|
if ($zeitpunkt > $jetzt) {
|
||||||
|
if ($vorherigerZeitpunkt === null) {
|
||||||
|
return $zeitpunkt - $jetzt <= $maximalerPunktabstandSekunden
|
||||||
|
? $vorgabe
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
if ($jetzt - $vorherigerZeitpunkt > $maximalerPunktabstandSekunden
|
||||||
|
|| $zeitpunkt - $jetzt > $maximalerPunktabstandSekunden
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anteil = ($jetzt - $vorherigerZeitpunkt) / ($zeitpunkt - $vorherigerZeitpunkt);
|
||||||
|
|
||||||
|
return $vorherigeVorgabe + (($vorgabe - $vorherigeVorgabe) * $anteil);
|
||||||
|
}
|
||||||
|
$vorherigerZeitpunkt = $zeitpunkt;
|
||||||
|
$vorherigeVorgabe = $vorgabe;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $vorherigerZeitpunkt !== null
|
||||||
|
&& $jetzt - $vorherigerZeitpunkt <= $maximalerPunktabstandSekunden
|
||||||
|
? $vorherigeVorgabe
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
|
* @param array<int, float|int> $monatsgrenzen Monatsnummer 1 bis 12 als Schluessel
|
||||||
*/
|
*/
|
||||||
@@ -92,6 +157,7 @@ final class ManagerRegler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
|
* @param list<array<string, mixed>> $verbraucher Vollstaendige, bereits gepruefte Verbraucherdaten
|
||||||
|
* @param array<int, float|int> $bezogeneEnergieWh Bezogene Energie je Verbraucher in Wh
|
||||||
* @return array{
|
* @return array{
|
||||||
* Verteilbudget_W: int,
|
* Verteilbudget_W: int,
|
||||||
* AktuelleVerbraucherleistung_W: int,
|
* AktuelleVerbraucherleistung_W: int,
|
||||||
@@ -104,7 +170,8 @@ final class ManagerRegler
|
|||||||
float $netzleistung,
|
float $netzleistung,
|
||||||
float $zielnetzleistung,
|
float $zielnetzleistung,
|
||||||
array $verbraucher,
|
array $verbraucher,
|
||||||
string $prioritaetsfeld
|
string $prioritaetsfeld,
|
||||||
|
array $bezogeneEnergieWh = []
|
||||||
): array {
|
): array {
|
||||||
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
|
if (!in_array($prioritaetsfeld, ['PrioritaetPV', 'PrioritaetPeak'], true)) {
|
||||||
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
|
throw new InvalidArgumentException('Unbekanntes Prioritaetsfeld.');
|
||||||
@@ -120,44 +187,107 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$festwert = self::istFestwert($daten['Leistungswerte_W']);
|
||||||
|
$festgehalten = !$daten['AenderungMoeglich'];
|
||||||
|
if ($festwert || $festgehalten) {
|
||||||
|
$nichtSteuerbar += $leistung;
|
||||||
|
}
|
||||||
|
|
||||||
|
$energie = $bezogeneEnergieWh[$eintrag['InstanzID']] ?? 0.0;
|
||||||
|
if ((!is_int($energie) && !is_float($energie))
|
||||||
|
|| (is_float($energie) && !is_finite($energie))
|
||||||
|
) {
|
||||||
|
throw new InvalidArgumentException('Bezogene Energie muss eine endliche Zahl sein.');
|
||||||
|
}
|
||||||
$steuerbar[] = [
|
$steuerbar[] = [
|
||||||
'InstanzID' => $eintrag['InstanzID'],
|
'InstanzID' => $eintrag['InstanzID'],
|
||||||
'Prioritaet' => $daten[$prioritaetsfeld],
|
'Prioritaet' => $daten[$prioritaetsfeld],
|
||||||
|
'Energiegruppe' => (int) round((float) $energie / 2000.0),
|
||||||
'Leistungswerte_W' => $daten['Leistungswerte_W'],
|
'Leistungswerte_W' => $daten['Leistungswerte_W'],
|
||||||
|
'Festwert' => $festwert,
|
||||||
|
'Festgehalten' => $festgehalten,
|
||||||
|
'Sollwert' => self::angenommenerSollwert($daten),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
usort($steuerbar, static function (array $links, array $rechts): int {
|
usort($steuerbar, static function (array $links, array $rechts): int {
|
||||||
return [$links['Prioritaet'], $links['InstanzID']] <=> [$rechts['Prioritaet'], $rechts['InstanzID']];
|
return [
|
||||||
|
$links['Prioritaet'],
|
||||||
|
$links['Energiegruppe'],
|
||||||
|
$links['InstanzID'],
|
||||||
|
] <=> [
|
||||||
|
$rechts['Prioritaet'],
|
||||||
|
$rechts['Energiegruppe'],
|
||||||
|
$rechts['InstanzID'],
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
|
$verteilbudget = (int) round($aktuelleLeistung + $zielnetzleistung - $netzleistung);
|
||||||
|
$positiveRichtung = $verteilbudget - $nichtSteuerbar >= 0;
|
||||||
$sollwerte = [];
|
$sollwerte = [];
|
||||||
$minimumSumme = 0;
|
$ausgangssumme = 0;
|
||||||
foreach ($steuerbar as $eintrag) {
|
foreach ($steuerbar as $eintrag) {
|
||||||
$minimum = self::kleinsterLeistungswert($eintrag['Leistungswerte_W']);
|
if ($eintrag['Festwert']) {
|
||||||
$sollwerte[$eintrag['InstanzID']] = $minimum;
|
$sollwerte[$eintrag['InstanzID']] = self::kleinsterLeistungswert(
|
||||||
$minimumSumme += $minimum;
|
$eintrag['Leistungswerte_W']
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($eintrag['Festgehalten']) {
|
||||||
|
if ($eintrag['Sollwert'] !== null) {
|
||||||
|
$sollwerte[$eintrag['InstanzID']] = $eintrag['Sollwert'];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$verfuegbar = max(0, $verteilbudget - $nichtSteuerbar - $minimumSumme);
|
$ausgangswert = self::richtungsneutralerLeistungswert(
|
||||||
|
$eintrag['Leistungswerte_W'],
|
||||||
|
$positiveRichtung
|
||||||
|
);
|
||||||
|
$sollwerte[$eintrag['InstanzID']] = $ausgangswert;
|
||||||
|
$ausgangssumme += $ausgangswert;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rest = $verteilbudget - $nichtSteuerbar - $ausgangssumme;
|
||||||
foreach ($steuerbar as $eintrag) {
|
foreach ($steuerbar as $eintrag) {
|
||||||
|
if ($eintrag['Festwert'] || $eintrag['Festgehalten']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$instanzID = $eintrag['InstanzID'];
|
$instanzID = $eintrag['InstanzID'];
|
||||||
$minimum = $sollwerte[$instanzID];
|
$ausgangswert = $sollwerte[$instanzID];
|
||||||
|
if ($positiveRichtung) {
|
||||||
|
if ($rest <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$neuerSollwert = self::groessterLeistungswertBis(
|
$neuerSollwert = self::groessterLeistungswertBis(
|
||||||
$eintrag['Leistungswerte_W'],
|
$eintrag['Leistungswerte_W'],
|
||||||
$minimum + $verfuegbar
|
$ausgangswert + $rest
|
||||||
);
|
);
|
||||||
$verfuegbar -= $neuerSollwert - $minimum;
|
} else {
|
||||||
|
if ($rest >= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$neuerSollwert = self::kleinsterLeistungswertAb(
|
||||||
|
$eintrag['Leistungswerte_W'],
|
||||||
|
$ausgangswert + $rest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$rest -= $neuerSollwert - $ausgangswert;
|
||||||
$sollwerte[$instanzID] = $neuerSollwert;
|
$sollwerte[$instanzID] = $neuerSollwert;
|
||||||
}
|
}
|
||||||
|
|
||||||
$zugeteilt = $nichtSteuerbar + array_sum($sollwerte);
|
$zugeteilt = $nichtSteuerbar;
|
||||||
|
foreach ($steuerbar as $eintrag) {
|
||||||
|
if (!$eintrag['Festwert'] && !$eintrag['Festgehalten']) {
|
||||||
|
$zugeteilt += $sollwerte[$eintrag['InstanzID']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'Verteilbudget_W' => $verteilbudget,
|
'Verteilbudget_W' => $verteilbudget,
|
||||||
@@ -214,6 +344,53 @@ final class ManagerRegler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einzelner Leistungswert bleibt als Befehl erhalten. Fuer die
|
||||||
|
* Leistungsbilanz zaehlt jedoch die gemessene Istleistung.
|
||||||
|
*
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
private static function istFestwert(array $leistungswerte): bool
|
||||||
|
{
|
||||||
|
return count($leistungswerte) === 1 && is_int($leistungswerte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
private static function richtungsneutralerLeistungswert(
|
||||||
|
array $leistungswerte,
|
||||||
|
bool $positiveRichtung
|
||||||
|
): int {
|
||||||
|
$negativ = null;
|
||||||
|
$positiv = null;
|
||||||
|
foreach ($leistungswerte as $eintrag) {
|
||||||
|
if (is_int($eintrag)) {
|
||||||
|
if ($eintrag === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$negativ = $eintrag < 0 ? max($negativ ?? $eintrag, $eintrag) : $negativ;
|
||||||
|
$positiv = $eintrag > 0 ? min($positiv ?? $eintrag, $eintrag) : $positiv;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($eintrag['Von_W'] <= 0 && $eintrag['Bis_W'] >= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
$negativ = $eintrag['Bis_W'] < 0
|
||||||
|
? max($negativ ?? $eintrag['Bis_W'], $eintrag['Bis_W'])
|
||||||
|
: $negativ;
|
||||||
|
$positiv = $eintrag['Von_W'] > 0
|
||||||
|
? min($positiv ?? $eintrag['Von_W'], $eintrag['Von_W'])
|
||||||
|
: $positiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($positiveRichtung) {
|
||||||
|
return $positiv ?? $negativ;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $negativ ?? $positiv;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
*/
|
*/
|
||||||
@@ -247,4 +424,26 @@ final class ManagerRegler
|
|||||||
|
|
||||||
return $gewaehlt;
|
return $gewaehlt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int|array{Von_W: int, Bis_W: int}> $leistungswerte
|
||||||
|
*/
|
||||||
|
private static function kleinsterLeistungswertAb(array $leistungswerte, int $untergrenze): int
|
||||||
|
{
|
||||||
|
foreach ($leistungswerte as $eintrag) {
|
||||||
|
if (is_int($eintrag)) {
|
||||||
|
if ($eintrag >= $untergrenze) {
|
||||||
|
return $eintrag;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($eintrag['Bis_W'] < $untergrenze) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max($eintrag['Von_W'], $untergrenze);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::richtungsneutralerLeistungswert($leistungswerte, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -77,6 +77,33 @@ final class EaseeLadestatusTest extends TestCase
|
|||||||
self::assertSame(0, $status['Phasenzahl']);
|
self::assertSame(0, $status['Phasenzahl']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testStillstehendeWallboxSetztVeralteteLeistungUndStromAufNull(): void
|
||||||
|
{
|
||||||
|
$status = EaseeLadestatus::ausBeobachtungen([
|
||||||
|
'109' => 2,
|
||||||
|
'110' => 30,
|
||||||
|
'120' => 10.294,
|
||||||
|
'182' => 14.9,
|
||||||
|
'183' => 14.8,
|
||||||
|
'184' => 14.7,
|
||||||
|
], 32);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
|
||||||
|
self::assertEqualsWithDelta(0.0, $status['Ladestrom_A'], 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFertigGeladeneWallboxSetztVeralteteLeistungAufNull(): void
|
||||||
|
{
|
||||||
|
$status = EaseeLadestatus::ausBeobachtungen([
|
||||||
|
'109' => 4,
|
||||||
|
'110' => 30,
|
||||||
|
'120' => 10.294,
|
||||||
|
], 32);
|
||||||
|
|
||||||
|
self::assertTrue($status['FahrzeugGeladen']);
|
||||||
|
self::assertEqualsWithDelta(0.0, $status['Istleistung_W'], 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
public function testApiUndKabelgrenzenBegrenzenDenKonfiguriertenStrom(): void
|
public function testApiUndKabelgrenzenBegrenzenDenKonfiguriertenStrom(): void
|
||||||
{
|
{
|
||||||
$status = EaseeLadestatus::ausBeobachtungen([
|
$status = EaseeLadestatus::ausBeobachtungen([
|
||||||
@@ -89,6 +116,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> */
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ final class LadestationAdapterTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @dataProvider statusFaelle
|
* @dataProvider statusFaelle
|
||||||
* @param array<string, string> $konfiguration
|
* @param array<string, string> $konfiguration
|
||||||
* @param array{Rohstatus: int, Istleistung_W: float} $erwartet
|
* @param array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int} $erwartet
|
||||||
*/
|
*/
|
||||||
public function testStatusaufrufUndDekodierungFuerAlleGeraete(
|
public function testStatusaufrufUndDekodierungFuerAlleGeraete(
|
||||||
int $geraetetyp,
|
int $geraetetyp,
|
||||||
@@ -47,16 +47,26 @@ final class LadestationAdapterTest extends TestCase
|
|||||||
yield 'go-e alt' => [
|
yield 'go-e alt' => [
|
||||||
LadestationAdapter::GO_E_ALT,
|
LadestationAdapter::GO_E_ALT,
|
||||||
$this->lokaleKonfiguration(),
|
$this->lokaleKonfiguration(),
|
||||||
$this->goEAntwort(2, 1100),
|
$this->goEAntwort(2, 1100, [160, 158, 159]),
|
||||||
'http://192.0.2.10/mqtt?payload=',
|
'http://192.0.2.10/mqtt?payload=',
|
||||||
['Rohstatus' => 2, 'Istleistung_W' => 11000.0],
|
[
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 11000.0,
|
||||||
|
'Iststrom_A' => 16.0,
|
||||||
|
'Phasenzahl' => 3,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
yield 'go-e Gemini' => [
|
yield 'go-e Gemini' => [
|
||||||
LadestationAdapter::GO_E_GEMINI,
|
LadestationAdapter::GO_E_GEMINI,
|
||||||
$this->lokaleKonfiguration(),
|
$this->lokaleKonfiguration(),
|
||||||
$this->goEAntwort(2, 2300),
|
$this->goEAntwort(2, 2300, [9.8, 0, 0]),
|
||||||
'http://192.0.2.10/api/status',
|
'http://192.0.2.10/api/status',
|
||||||
['Rohstatus' => 2, 'Istleistung_W' => 2300.0],
|
[
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 2300.0,
|
||||||
|
'Iststrom_A' => 9.8,
|
||||||
|
'Phasenzahl' => 1,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
yield 'Smart-me Pico' => [
|
yield 'Smart-me Pico' => [
|
||||||
LadestationAdapter::SMART_ME_PICO,
|
LadestationAdapter::SMART_ME_PICO,
|
||||||
@@ -161,9 +171,13 @@ final class LadestationAdapterTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function goEAntwort(int $status, float $leistung): string
|
/** @param list<int|float> $stroeme */
|
||||||
|
private function goEAntwort(int $status, float $leistung, array $stroeme): string
|
||||||
{
|
{
|
||||||
$nrg = array_fill(0, 12, 0);
|
$nrg = array_fill(0, 12, 0);
|
||||||
|
foreach (array_values($stroeme) as $index => $strom) {
|
||||||
|
$nrg[4 + $index] = $strom;
|
||||||
|
}
|
||||||
$nrg[11] = $leistung;
|
$nrg[11] = $leistung;
|
||||||
|
|
||||||
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
||||||
|
|||||||
@@ -22,28 +22,111 @@ final class LadestationReglerTest extends TestCase
|
|||||||
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
|
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEinphasigesFahrzeugWirdUnterhalb7500WErkannt(): void
|
public function testDirektGemeldeteEinphasigkeitWirdAuchUnter7500WErkannt(): void
|
||||||
{
|
{
|
||||||
$status = LadestationRegler::normalisiereStatus([
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
'Rohstatus' => 2,
|
'Rohstatus' => 2,
|
||||||
'Istleistung_W' => 2300.0,
|
'Istleistung_W' => 2300.0,
|
||||||
|
'Phasenzahl' => 1,
|
||||||
], false);
|
], false);
|
||||||
|
|
||||||
self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']);
|
self::assertSame(LadestationRegler::PHASEN_EIN, $status['Phasenzahl']);
|
||||||
self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01);
|
self::assertEqualsWithDelta(10.0, $status['Ladestrom_A'], 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGeladenWirdNachVerbindungUnterSechsAmpereErkannt(): void
|
public function testErkannteDreiphasigkeitBleibtWaehrendLadepauseErhalten(): void
|
||||||
{
|
{
|
||||||
$status = LadestationRegler::normalisiereStatus([
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
'Rohstatus' => 2,
|
'Rohstatus' => 2,
|
||||||
'Istleistung_W' => 0.0,
|
'Istleistung_W' => 0.0,
|
||||||
], true);
|
], true, false, false, true, LadestationRegler::PHASEN_DREI);
|
||||||
|
|
||||||
|
self::assertSame(LadestationRegler::PHASEN_DREI, $status['Phasenzahl']);
|
||||||
|
self::assertSame(0.0, $status['Ladestrom_A']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPhasenBleibenOhneMessungBisZurErkennungUnbekannt(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 0.0,
|
||||||
|
], false);
|
||||||
|
|
||||||
|
self::assertSame(LadestationRegler::PHASEN_UNBEKANNT, $status['Phasenzahl']);
|
||||||
|
self::assertSame([0], LadestationRegler::leistungsangebot(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
LadestationRegler::PHASEN_UNBEKANNT,
|
||||||
|
16,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnlaufOhneGemesseneLadeleistungGiltNichtAlsLadeende(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 0.0,
|
||||||
|
], true, false, true, false);
|
||||||
|
|
||||||
|
self::assertFalse($status['FahrzeugGeladen']);
|
||||||
|
self::assertSame(2, $status['Fahrzeugstatus']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGeladenWirdErstNachZuvorGemessenerLadeleistungErkannt(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 0.0,
|
||||||
|
], true, false, true, true);
|
||||||
|
|
||||||
self::assertTrue($status['FahrzeugGeladen']);
|
self::assertTrue($status['FahrzeugGeladen']);
|
||||||
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 testDirekterPhasenstromHatVorrangVorNominalerLeistungsumrechnung(): void
|
||||||
|
{
|
||||||
|
$status = LadestationRegler::normalisiereStatus([
|
||||||
|
'Rohstatus' => 2,
|
||||||
|
'Istleistung_W' => 10263.0,
|
||||||
|
'Iststrom_A' => 15.9,
|
||||||
|
], true);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(15.9, $status['Ladestrom_A'], 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFahrzeugMaximalstromWirdMitZweiAmpereAbstandErkannt(): void
|
||||||
|
{
|
||||||
|
self::assertSame(18, LadestationRegler::ermittleFahrzeugMaximalstrom(20, 15.9, 20));
|
||||||
|
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(17, 15.9, 20));
|
||||||
|
self::assertNull(LadestationRegler::ermittleFahrzeugMaximalstrom(20, 0.0, 20));
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -53,7 +136,7 @@ final class LadestationReglerTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame(0, $solarPV[0]);
|
self::assertSame(0, $solarPV[0]);
|
||||||
self::assertCount(12, $solarPV);
|
self::assertCount(12, $solarPV);
|
||||||
self::assertSame([3850], $normalPV);
|
self::assertSame([3680], $normalPV);
|
||||||
self::assertSame([0], $solarPeak);
|
self::assertSame([0], $solarPeak);
|
||||||
self::assertSame($solarPV, $normalPeak);
|
self::assertSame($solarPV, $normalPeak);
|
||||||
}
|
}
|
||||||
@@ -61,7 +144,7 @@ final class LadestationReglerTest extends TestCase
|
|||||||
public function testPeakKannJedesFahrzeugAufNullSetzen(): void
|
public function testPeakKannJedesFahrzeugAufNullSetzen(): void
|
||||||
{
|
{
|
||||||
self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3));
|
self::assertSame(0, LadestationRegler::stromFuerLeistung(0, 3));
|
||||||
self::assertSame(6, LadestationRegler::stromFuerLeistung(4150, 3));
|
self::assertSame(6, LadestationRegler::stromFuerLeistung(4104, 3));
|
||||||
self::assertSame(16, LadestationRegler::stromFuerLeistung(11000, 3));
|
self::assertSame(16, LadestationRegler::stromFuerLeistung(10944, 3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,22 @@ final class LadestationStandAloneModulstrukturTest extends TestCase
|
|||||||
"RegisterPropertyBoolean('Ladefreigabe', true)",
|
"RegisterPropertyBoolean('Ladefreigabe', true)",
|
||||||
$modul
|
$modul
|
||||||
);
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('Phasenerkennungszeit', 60)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('ZeitZwischenZustandswechseln', 1)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('Mindesteinschaltdauer', 0)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyInteger('Mindestausschaltdauer', 0)",
|
||||||
|
$modul
|
||||||
|
);
|
||||||
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
|
self::assertStringContainsString("EnableAction('Ladefreigabe')", $modul);
|
||||||
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
self::assertStringContainsString("EnableAction('Solarladen')", $modul);
|
||||||
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
self::assertStringContainsString("ReadPropertyBoolean('LoggingEin')", $modul);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
|||||||
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
|
"RegisterPropertyBoolean('EnergiediagrammeAnzeigen', false)",
|
||||||
"RegisterPropertyBoolean('FunFactsAnzeigen', false)",
|
"RegisterPropertyBoolean('FunFactsAnzeigen', false)",
|
||||||
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
|
"RegisterPropertyBoolean('EnergieflussAnzeigen', false)",
|
||||||
|
"RegisterPropertyInteger('EnergieflussLeistungseinheit', 0)",
|
||||||
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
|
"RegisterPropertyInteger('LeistungsaufzeichnungMinuten', 1)",
|
||||||
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
|
"RegisterPropertyInteger('LeistungLoeschenMonate', 12)",
|
||||||
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
|
"RegisterPropertyInteger('EnergieaufzeichnungMinuten', 5)",
|
||||||
@@ -48,6 +49,7 @@ final class ManagerEnergieModulstrukturTest extends TestCase
|
|||||||
'FormEnergiediagrammeAnzeigen',
|
'FormEnergiediagrammeAnzeigen',
|
||||||
'FormFunFactsAnzeigen',
|
'FormFunFactsAnzeigen',
|
||||||
'FormEnergieflussAnzeigen',
|
'FormEnergieflussAnzeigen',
|
||||||
|
'EnergieflussLeistungseinheit',
|
||||||
'MesswerteAnzeigen',
|
'MesswerteAnzeigen',
|
||||||
'IstleistungVariableID',
|
'IstleistungVariableID',
|
||||||
'LeistungVariableID',
|
'LeistungVariableID',
|
||||||
|
|||||||
@@ -218,14 +218,28 @@ final class ManagerModulstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString("'issueDeviceToken' =>", $inhalt);
|
self::assertStringContainsString("'issueDeviceToken' =>", $inhalt);
|
||||||
self::assertStringContainsString("unset(\$neueLease['deviceToken'])", $inhalt);
|
self::assertStringContainsString("unset(\$neueLease['deviceToken'])", $inhalt);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))",
|
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv')",
|
||||||
$inhalt
|
$inhalt
|
||||||
);
|
);
|
||||||
|
self::assertGreaterThanOrEqual(
|
||||||
|
3,
|
||||||
|
substr_count($inhalt, '$this->geraetezugangBenoetigt()'),
|
||||||
|
'Der Geraetezugang muss beim Anwenden, Erneuern und Anfordern beruecksichtigt werden.'
|
||||||
|
);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
"\$this->ReadPropertyString('Lizenzcode'),\n \$geraetezugangFehlt",
|
"\$this->ReadPropertyString('Lizenzcode'),\n \$geraetezugangFehlt",
|
||||||
$inhalt
|
$inhalt
|
||||||
);
|
);
|
||||||
self::assertStringContainsString('synchronisiereAnlagentopologie()', $inhalt);
|
self::assertStringContainsString('synchronisiereAnlagentopologie()', $inhalt);
|
||||||
|
self::assertStringContainsString('private function geraetezugangBenoetigt(): bool', $inhalt);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"if (\$this->aktualisiereLizenzfreigabe(\$wert, true)) {",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringNotContainsString(
|
||||||
|
"if (!\$this->ReadPropertyBoolean('PrognoseAktiv')) {\n \$this->setzeDiagnosewert('Prognosestatus', 'NichtVerwendet');\n return;\n }\n\n try {",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
self::assertStringContainsString(
|
self::assertStringContainsString(
|
||||||
'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry',
|
'https://license.enelix.ch/api/v1/installations/%s/prognosis/telemetry',
|
||||||
$inhalt
|
$inhalt
|
||||||
@@ -254,6 +268,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,13 +313,17 @@ 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($inhalt, '$this->geraetezugangBenoetigt()'),
|
||||||
$inhalt,
|
'Der Geraetezugang muss beim Anwenden, Erneuern und Anfordern beruecksichtigt werden.'
|
||||||
"|| \$this->ReadPropertyBoolean('StoerueberwachungAktiv'))"
|
|
||||||
),
|
|
||||||
'Der Gerätezugang muss beim Anwenden, Erneuern und Anfordern berücksichtigt werden.'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+273
-4
@@ -11,6 +11,59 @@ use PHPUnit\Framework\TestCase;
|
|||||||
|
|
||||||
final class ManagerReglerTest extends TestCase
|
final class ManagerReglerTest extends TestCase
|
||||||
{
|
{
|
||||||
|
public function testFahrplanVorgabeWirdZwischenPunktenLinearInterpoliert(): void
|
||||||
|
{
|
||||||
|
$punkte = [
|
||||||
|
['time' => '2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254],
|
||||||
|
['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283],
|
||||||
|
];
|
||||||
|
|
||||||
|
$vorgabe = ManagerRegler::interpolierteFahrplanVorgabe(
|
||||||
|
$punkte,
|
||||||
|
(int) strtotime('2026-09-30T11:32:30+00:00')
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertEqualsWithDelta(18075.2685, $vorgabe, 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFahrplanVorgabeVerwendetRandpunktInnerhalbDerToleranz(): void
|
||||||
|
{
|
||||||
|
$punkte = [
|
||||||
|
['time' => '2026-09-30T11:30:00+00:00', 'gridTargetW' => 15664.283],
|
||||||
|
['time' => '2026-09-30T11:35:00+00:00', 'gridTargetW' => 20486.254],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
15664.283,
|
||||||
|
ManagerRegler::interpolierteFahrplanVorgabe(
|
||||||
|
$punkte,
|
||||||
|
(int) strtotime('2026-09-30T11:29:44+00:00')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
20486.254,
|
||||||
|
ManagerRegler::interpolierteFahrplanVorgabe(
|
||||||
|
$punkte,
|
||||||
|
(int) strtotime('2026-09-30T11:35:10+00:00')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFahrplanVorgabeVerwirftZuGrosseZeitlicheLuecke(): void
|
||||||
|
{
|
||||||
|
$punkte = [
|
||||||
|
['time' => '2026-09-30T11:00:00+00:00', 'gridTargetW' => 10000],
|
||||||
|
['time' => '2026-09-30T12:00:00+00:00', 'gridTargetW' => 20000],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertNull(
|
||||||
|
ManagerRegler::interpolierteFahrplanVorgabe(
|
||||||
|
$punkte,
|
||||||
|
(int) strtotime('2026-09-30T11:31:00+00:00')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void
|
public function testOhnePeakBegrenzungBleibtSolarladenAktiv(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
@@ -113,21 +166,215 @@ final class ManagerReglerTest extends TestCase
|
|||||||
self::assertSame(500, $ergebnis['Restabweichung_W']);
|
self::assertSame(500, $ergebnis['Restabweichung_W']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNichtAenderbarerVerbraucherReserviertSeineIstleistung(): void
|
public function testGleichePrioritaetBevorzugtWieEnelix1DenVerbraucherMitWenigerEnergie(): void
|
||||||
{
|
{
|
||||||
$fest = $this->verbraucher(10, 0, [0, 3000], 3000, false);
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
0.0,
|
||||||
|
3000.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(10, 0, [0, 3000], 0),
|
||||||
|
$this->verbraucher(20, 0, [0, 3000], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV',
|
||||||
|
[10 => 3000.0, 20 => 0.0]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([20 => 3000, 10 => 0], $ergebnis['Sollwerte']);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnergievergleichVerwendetWieEnelix1ZweiKilowattstundenGruppen(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
0.0,
|
||||||
|
3000.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(10, 0, [0, 3000], 0),
|
||||||
|
$this->verbraucher(20, 0, [0, 3000], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV',
|
||||||
|
[10 => 999.0, 20 => 0.0]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([10 => 3000, 20 => 0], $ergebnis['Sollwerte']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFestwertWirdUnabhaengigVonAenderungMoeglichZugewiesen(): void
|
||||||
|
{
|
||||||
|
$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(3000, $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 testFestwerteWerdenNachIstleistungBilanziertUndAlsBefehlBeibehalten(): 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(10151, $ergebnis['NichtSteuerbar_W']);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
44234 => 2411,
|
||||||
|
29743 => 0,
|
||||||
|
38676 => 0,
|
||||||
|
43878 => 13850,
|
||||||
|
46052 => 11000,
|
||||||
|
],
|
||||||
|
$ergebnis['Sollwerte']
|
||||||
|
);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBatterieGleichtGemesseneFesteLastenAusOhneSolarladungZuSpeisen(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
-4526.0,
|
||||||
|
-50.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(29743, 1, [0, 6500], 6500),
|
||||||
|
$this->verbraucher(38676, 1, [0, 4600], 4600),
|
||||||
|
$this->verbraucher(22078, 2, [7690], 0, false),
|
||||||
|
$this->verbraucher(31986, 2, [11000], 10942, false),
|
||||||
|
$this->verbraucher(43878, 2, [0, 4150, 10300], 9908),
|
||||||
|
$this->verbraucher(
|
||||||
|
44234,
|
||||||
|
6,
|
||||||
|
[['Von_W' => -39000, 'Bis_W' => 39000]],
|
||||||
|
-22769
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(13657, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame(10942, $ergebnis['NichtSteuerbar_W']);
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
29743 => 0,
|
||||||
|
38676 => 0,
|
||||||
|
22078 => 7690,
|
||||||
|
31986 => 11000,
|
||||||
|
43878 => 0,
|
||||||
|
44234 => 2715,
|
||||||
|
],
|
||||||
|
$ergebnis['Sollwerte']
|
||||||
|
);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBegrenztesFahrzeugLaedtVorBatterieOhneBatterieentladung(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
-3587.0,
|
||||||
|
0.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
[0, 4150, 4850, 5550, 6250, 6950, 7600, 8300, 9000, 9700, 10300, 11000],
|
||||||
|
10263
|
||||||
|
),
|
||||||
|
$this->verbraucher(
|
||||||
|
20,
|
||||||
|
1,
|
||||||
|
[['Von_W' => -39000, 'Bis_W' => 39000]],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(13850, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame([10 => 11000, 20 => 2850], $ergebnis['Sollwerte']);
|
||||||
|
self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][20]);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPvBetriebNutztBeiPositivemBudgetKeineNegativeLeistungsstufe(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
0.0,
|
||||||
|
500.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(10, 0, [-1000, 5000], 0),
|
||||||
|
$this->verbraucher(20, 1, [0, 3000], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(500, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame([10 => 5000, 20 => 0], $ergebnis['Sollwerte']);
|
||||||
|
self::assertGreaterThanOrEqual(0, $ergebnis['Sollwerte'][10]);
|
||||||
|
self::assertSame(-4500, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPvBetriebNutztNegativeLeistungErstBeiTatsaechlichemDefizit(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
1000.0,
|
||||||
|
0.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
[['Von_W' => -5000, 'Bis_W' => 5000]],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
$this->verbraucher(20, 1, [0, 3000], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(-1000, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame([10 => -1000, 20 => 0], $ergebnis['Sollwerte']);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFestgehaltenerVerbraucherBilanziertIstUndBehaeltSollwert(): void
|
||||||
|
{
|
||||||
|
$festgehalten = $this->verbraucher(10, 0, [0, 3000, 5000], 2000, false);
|
||||||
|
$festgehalten['Daten']['Zustand'][0]['Wert'] = 3000;
|
||||||
|
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
0.0,
|
||||||
|
3000.0,
|
||||||
|
[
|
||||||
|
$festgehalten,
|
||||||
|
$this->verbraucher(20, 1, [['Von_W' => 0, 'Bis_W' => 5000]], 0),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(5000, $ergebnis['Verteilbudget_W']);
|
||||||
|
self::assertSame(2000, $ergebnis['NichtSteuerbar_W']);
|
||||||
|
self::assertSame([10 => 3000, 20 => 3000], $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,9 +382,31 @@ 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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testBatterieMitTieferPrioritaetUebernimmtNichtNutzbarenRest(): void
|
||||||
|
{
|
||||||
|
$ergebnis = ManagerRegler::verteile(
|
||||||
|
-5000.0,
|
||||||
|
0.0,
|
||||||
|
[
|
||||||
|
$this->verbraucher(20, 1, [0, 3000, 6000], 0),
|
||||||
|
$this->verbraucher(
|
||||||
|
10,
|
||||||
|
6,
|
||||||
|
[['Von_W' => 0, 'Bis_W' => 10000]],
|
||||||
|
0
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'PrioritaetPV'
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([20 => 3000, 10 => 2000], $ergebnis['Sollwerte']);
|
||||||
|
self::assertSame(0, $ergebnis['Restabweichung_W']);
|
||||||
|
}
|
||||||
|
|
||||||
public function testMonatlicheGrenzeMussVollstaendigSein(): void
|
public function testMonatlicheGrenzeMussVollstaendigSein(): void
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|||||||
@@ -24,6 +24,38 @@ return static function (TestContext $test): void {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('SignalR-Abbruch synchronisiert den Gatewaystatus', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($gateway): void {
|
||||||
|
IPS_RequestAction(
|
||||||
|
$gateway,
|
||||||
|
'TestSignalRFrame',
|
||||||
|
json_encode(['type' => 7], JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->assertInstanceStatus($gateway, 203);
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
GetValue($test->objectByIdent('Connected', $gateway))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
'Easee-Ereignisverbindung wurde beendet.',
|
||||||
|
GetValue($test->objectByIdent('LastError', $gateway))
|
||||||
|
);
|
||||||
|
|
||||||
|
IPS_RequestAction($gateway, 'TestSignalRFrame', '{}');
|
||||||
|
|
||||||
|
$test->assertInstanceStatus($gateway);
|
||||||
|
$test->assertSame(
|
||||||
|
true,
|
||||||
|
GetValue($test->objectByIdent('Connected', $gateway))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
'',
|
||||||
|
GetValue($test->objectByIdent('LastError', $gateway))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$test->runCase('Abonnement liefert injizierten API-Zustand', static function (
|
$test->runCase('Abonnement liefert injizierten API-Zustand', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use ($gateway): void {
|
) use ($gateway): void {
|
||||||
@@ -72,6 +104,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 {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ return static function (TestContext $test): void {
|
|||||||
) use ($station): void {
|
) use ($station): void {
|
||||||
IPS_RequestAction($station, 'Aktiv', true);
|
IPS_RequestAction($station, 'Aktiv', true);
|
||||||
$test->assertSame(
|
$test->assertSame(
|
||||||
'[0,4150,4850,5550,6250,6950,7600,8300,9000,9700,10300,11000]',
|
'[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944]',
|
||||||
(string) GetValue(
|
(string) GetValue(
|
||||||
$test->objectByIdent('LeistungsangebotDiagnose', $station)
|
$test->objectByIdent('LeistungsangebotDiagnose', $station)
|
||||||
)
|
)
|
||||||
@@ -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(
|
||||||
|
'[3680]',
|
||||||
|
(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 {
|
||||||
@@ -136,6 +184,32 @@ return static function (TestContext $test): void {
|
|||||||
$test->assertInstanceStatus($station);
|
$test->assertInstanceStatus($station);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('SignalR-Abbruch erreicht die Ladestation', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($gateway, $station): void {
|
||||||
|
IPS_RequestAction(
|
||||||
|
$gateway,
|
||||||
|
'TestSignalRFrame',
|
||||||
|
json_encode(['type' => 7], JSON_THROW_ON_ERROR)
|
||||||
|
);
|
||||||
|
|
||||||
|
$test->assertInstanceStatus($gateway, 203);
|
||||||
|
$test->assertInstanceStatus($station, 202);
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
GetValue($test->objectByIdent('GatewayVerbunden', $station))
|
||||||
|
);
|
||||||
|
|
||||||
|
IPS_RequestAction($gateway, 'TestSignalRFrame', '{}');
|
||||||
|
|
||||||
|
$test->assertInstanceStatus($gateway);
|
||||||
|
$test->assertInstanceStatus($station);
|
||||||
|
$test->assertSame(
|
||||||
|
true,
|
||||||
|
GetValue($test->objectByIdent('GatewayVerbunden', $station))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$test->runCase('Gateway-Ausfall verwirft Fahrzeug- und Phasenstatus', static function (
|
$test->runCase('Gateway-Ausfall verwirft Fahrzeug- und Phasenstatus', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
) use ($gateway, $station): void {
|
) use ($gateway, $station): void {
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ use Belevo\EnelixEMS\SymconTest\TestContext;
|
|||||||
|
|
||||||
return static function (TestContext $test): void {
|
return static function (TestContext $test): void {
|
||||||
$testPasswort = hash('sha256', __FILE__);
|
$testPasswort = hash('sha256', __FILE__);
|
||||||
$goEAntwort = static function (int $status, float $leistung): string {
|
$goEAntwort = static function (int $status, float $leistung, array $stroeme = []): string {
|
||||||
$nrg = array_fill(0, 12, 0);
|
$nrg = array_fill(0, 12, 0);
|
||||||
|
foreach (array_values($stroeme) as $index => $strom) {
|
||||||
|
$nrg[4 + $index] = $strom;
|
||||||
|
}
|
||||||
$nrg[11] = $leistung;
|
$nrg[11] = $leistung;
|
||||||
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
return json_encode(['car' => $status, 'nrg' => $nrg], JSON_THROW_ON_ERROR);
|
||||||
};
|
};
|
||||||
@@ -16,14 +19,14 @@ return static function (TestContext $test): void {
|
|||||||
'go-e alt' => [
|
'go-e alt' => [
|
||||||
'Geraetetyp' => 1,
|
'Geraetetyp' => 1,
|
||||||
'Geraeteadresse' => '192.0.2.10',
|
'Geraeteadresse' => '192.0.2.10',
|
||||||
'Testantwort' => $goEAntwort(2, 1100),
|
'Testantwort' => $goEAntwort(2, 1100, [160, 158, 159]),
|
||||||
'Phasenzahl' => 3,
|
'Phasenzahl' => 3,
|
||||||
'BefehlUrlTeil' => 'amp=16',
|
'BefehlUrlTeil' => 'amp=16',
|
||||||
],
|
],
|
||||||
'go-e Gemini' => [
|
'go-e Gemini' => [
|
||||||
'Geraetetyp' => 2,
|
'Geraetetyp' => 2,
|
||||||
'Geraeteadresse' => '192.0.2.11',
|
'Geraeteadresse' => '192.0.2.11',
|
||||||
'Testantwort' => $goEAntwort(2, 2300),
|
'Testantwort' => $goEAntwort(2, 2300, [10, 0, 0]),
|
||||||
'Phasenzahl' => 1,
|
'Phasenzahl' => 1,
|
||||||
'BefehlUrlTeil' => 'amp=16',
|
'BefehlUrlTeil' => 'amp=16',
|
||||||
],
|
],
|
||||||
@@ -47,6 +50,7 @@ return static function (TestContext $test): void {
|
|||||||
foreach ($definitionen as $name => $definition) {
|
foreach ($definitionen as $name => $definition) {
|
||||||
$properties = array_merge($definition, [
|
$properties = array_merge($definition, [
|
||||||
'MaximalerLadestrom' => 16,
|
'MaximalerLadestrom' => 16,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
'Ladefreigabe' => true,
|
'Ladefreigabe' => true,
|
||||||
'Solarladen' => true,
|
'Solarladen' => true,
|
||||||
'EinstellungenInVisu' => true,
|
'EinstellungenInVisu' => true,
|
||||||
@@ -61,6 +65,113 @@ 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,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 0),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$stromerkennung = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation Fahrzeugstromerkennung',
|
||||||
|
[
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.13',
|
||||||
|
'MaximalerLadestrom' => 20,
|
||||||
|
'FahrzeugstromErkennungszeit' => 1,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 10260, [15.9, 15.8, 15.7]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$phasenerkennung = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation Phasenerkennung',
|
||||||
|
[
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.14',
|
||||||
|
'MaximalerLadestrom' => 16,
|
||||||
|
'Phasenerkennungszeit' => 1,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 0),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$phasenhaltung = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation Phasenhaltung',
|
||||||
|
[
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.14',
|
||||||
|
'MaximalerLadestrom' => 16,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$schaltsperre = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation Schaltsperre',
|
||||||
|
[
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.15',
|
||||||
|
'MaximalerLadestrom' => 16,
|
||||||
|
'ZeitZwischenZustandswechseln' => 1,
|
||||||
|
'Mindesteinschaltdauer' => 2,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$mindestzeiten = $test->createInstance(
|
||||||
|
'{0D94913C-0F31-4C29-A685-6EB4AE58E55D}',
|
||||||
|
'Ladestation Mindestzeiten',
|
||||||
|
[
|
||||||
|
'Geraetetyp' => 2,
|
||||||
|
'Geraeteadresse' => '192.0.2.17',
|
||||||
|
'MaximalerLadestrom' => 16,
|
||||||
|
'ZeitZwischenZustandswechseln' => 0,
|
||||||
|
'Mindesteinschaltdauer' => 1,
|
||||||
|
'Mindestausschaltdauer' => 1,
|
||||||
|
'Ladefreigabe' => true,
|
||||||
|
'Solarladen' => true,
|
||||||
|
'EinstellungenInVisu' => true,
|
||||||
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
|
'Testmodus' => true,
|
||||||
|
'Testantwort' => $goEAntwort(2, 10944, [16, 16, 16]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
$managerId = $test->createInstance(
|
$managerId = $test->createInstance(
|
||||||
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}',
|
||||||
'Hilfsmanager Ladestation',
|
'Hilfsmanager Ladestation',
|
||||||
@@ -68,7 +179,16 @@ return static function (TestContext $test): void {
|
|||||||
'AutomatischeSuche' => false,
|
'AutomatischeSuche' => false,
|
||||||
'VerbraucherZuordnung' => json_encode(array_map(
|
'VerbraucherZuordnung' => json_encode(array_map(
|
||||||
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
static fn (int $id): array => ['InstanzID' => $id, 'Aktiv' => true],
|
||||||
array_values($stationen)
|
array_merge(
|
||||||
|
array_values($stationen),
|
||||||
|
[
|
||||||
|
$stromerkennung,
|
||||||
|
$phasenerkennung,
|
||||||
|
$phasenhaltung,
|
||||||
|
$schaltsperre,
|
||||||
|
$mindestzeiten,
|
||||||
|
]
|
||||||
|
)
|
||||||
), JSON_THROW_ON_ERROR),
|
), JSON_THROW_ON_ERROR),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -120,12 +240,115 @@ return static function (TestContext $test): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('Erkannte Phasen bleiben waehrend einer Ladepause stabil', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($goEAntwort, $phasenhaltung): void {
|
||||||
|
IPS_RequestAction($phasenhaltung, 'Aktiv', true);
|
||||||
|
$test->assertSame(
|
||||||
|
3,
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $phasenhaltung))
|
||||||
|
);
|
||||||
|
|
||||||
|
IPS_SetProperty($phasenhaltung, 'Testantwort', $goEAntwort(2, 0));
|
||||||
|
IPS_ApplyChanges($phasenhaltung);
|
||||||
|
IPS_RequestAction($phasenhaltung, 'Statuszyklus', false);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
3,
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $phasenhaltung))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
GetValue($test->objectByIdent('FahrzeugGeladen', $phasenhaltung))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Schaltsperre haelt die aktuelle Leistung stabil', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($schaltsperre, $anforderungSenden): void {
|
||||||
|
IPS_RequestAction($schaltsperre, 'Aktiv', true);
|
||||||
|
IPS_RequestAction($schaltsperre, 'Solarladen', true);
|
||||||
|
$anforderungSenden($schaltsperre, 4104);
|
||||||
|
IPS_RequestAction($schaltsperre, 'Statuszyklus', false);
|
||||||
|
|
||||||
|
$diagnose = json_encode([
|
||||||
|
'Angebot' => GetValue($test->objectByIdent('LeistungsangebotDiagnose', $schaltsperre)),
|
||||||
|
'Verbunden' => GetValue($test->objectByIdent('FahrzeugVerbunden', $schaltsperre)),
|
||||||
|
'Geladen' => GetValue($test->objectByIdent('FahrzeugGeladen', $schaltsperre)),
|
||||||
|
'Phasen' => GetValue($test->objectByIdent('Phasenzahl', $schaltsperre)),
|
||||||
|
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $schaltsperre)),
|
||||||
|
'SollwertGueltig' => GetValue($test->objectByIdent('SollwertGueltig', $schaltsperre)),
|
||||||
|
'Befehl' => GetValue($test->objectByIdent('LetzterGeraetebefehl', $schaltsperre)),
|
||||||
|
], JSON_THROW_ON_ERROR);
|
||||||
|
$test->assertSame(
|
||||||
|
'[4104]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $schaltsperre)),
|
||||||
|
'Diagnose: ' . $diagnose
|
||||||
|
);
|
||||||
|
|
||||||
|
$abgewiesen = false;
|
||||||
|
try {
|
||||||
|
$anforderungSenden($schaltsperre, 4788);
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$abgewiesen = true;
|
||||||
|
}
|
||||||
|
if (!$abgewiesen) {
|
||||||
|
$abgewiesen = GetValue(
|
||||||
|
$test->objectByIdent('SollwertGueltig', $schaltsperre)
|
||||||
|
) !== true || GetValue(
|
||||||
|
$test->objectByIdent('Sollleistung', $schaltsperre)
|
||||||
|
) !== 4788;
|
||||||
|
}
|
||||||
|
$test->assertSame(true, $abgewiesen);
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Mindestzeiten erlauben Modulation aber verhindern Takten', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($mindestzeiten, $anforderungSenden): void {
|
||||||
|
IPS_RequestAction($mindestzeiten, 'Aktiv', true);
|
||||||
|
$anforderungSenden($mindestzeiten, 4104);
|
||||||
|
IPS_RequestAction($mindestzeiten, 'Statuszyklus', false);
|
||||||
|
|
||||||
|
$angebot = json_decode(
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $mindestzeiten)),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$test->assertTrue(!in_array(0, $angebot, true));
|
||||||
|
$test->assertTrue(in_array(4788, $angebot, true));
|
||||||
|
|
||||||
|
$anforderungSenden($mindestzeiten, 4788);
|
||||||
|
IPS_RequestAction($mindestzeiten, 'Ladefreigabe', false);
|
||||||
|
IPS_RequestAction($mindestzeiten, 'Ladefreigabe', true);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
'[0]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $mindestzeiten))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Solarpause wird nicht als Ladeende erkannt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($solarPause): void {
|
||||||
|
IPS_RequestAction($solarPause, 'Solarladen', true);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
GetValue($test->objectByIdent('FahrzeugGeladen', $solarPause))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
2,
|
||||||
|
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 {
|
||||||
$stationId = $stationen['go-e alt'];
|
$stationId = $stationen['go-e alt'];
|
||||||
IPS_RequestAction($stationId, 'Solarladen', true);
|
IPS_RequestAction($stationId, 'Solarladen', true);
|
||||||
$anforderungSenden($stationId, 4150);
|
$anforderungSenden($stationId, 4104);
|
||||||
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
$befehl = (string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId));
|
||||||
$diagnose = json_encode([
|
$diagnose = json_encode([
|
||||||
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)),
|
'Sollleistung' => GetValue($test->objectByIdent('Sollleistung', $stationId)),
|
||||||
@@ -140,22 +363,22 @@ return static function (TestContext $test): void {
|
|||||||
|
|
||||||
IPS_RequestAction($stationId, 'Solarladen', false);
|
IPS_RequestAction($stationId, 'Solarladen', false);
|
||||||
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
|
$angebot = (string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId));
|
||||||
$test->assertSame('[11000]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
|
$test->assertSame('[10944]', $angebot, 'PV ohne Solarladen muss nur das Maximum anbieten.');
|
||||||
$abgewiesen = false;
|
$abgewiesen = false;
|
||||||
try {
|
try {
|
||||||
$anforderungSenden($stationId, 4150);
|
$anforderungSenden($stationId, 4104);
|
||||||
} catch (Throwable $fehler) {
|
} catch (Throwable $fehler) {
|
||||||
$abgewiesen = true;
|
$abgewiesen = true;
|
||||||
}
|
}
|
||||||
if (!$abgewiesen) {
|
if (!$abgewiesen) {
|
||||||
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $stationId)) !== true
|
$abgewiesen = GetValue($test->objectByIdent('SollwertGueltig', $stationId)) !== true
|
||||||
|| GetValue($test->objectByIdent('Sollleistung', $stationId)) !== 4150;
|
|| GetValue($test->objectByIdent('Sollleistung', $stationId)) !== 4104;
|
||||||
}
|
}
|
||||||
$test->assertSame(true, $abgewiesen, 'Normalbetrieb muss eine Zwischenstufe ablehnen.');
|
$test->assertSame(true, $abgewiesen, 'Normalbetrieb muss eine Zwischenstufe ablehnen.');
|
||||||
|
|
||||||
$anforderungSenden($stationId, 4150, 'Peak');
|
$anforderungSenden($stationId, 4104, 'Peak');
|
||||||
$test->assertSame(
|
$test->assertSame(
|
||||||
'[0,4150,4850,5550,6250,6950,7600,8300,9000,9700,10300,11000]',
|
'[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944]',
|
||||||
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
||||||
);
|
);
|
||||||
$test->assertTrue(str_contains(
|
$test->assertTrue(str_contains(
|
||||||
@@ -174,4 +397,79 @@ return static function (TestContext $test): void {
|
|||||||
'alw=0'
|
'alw=0'
|
||||||
), 'Peak mit Solarladen muss die Ladestation stoppen.');
|
), 'Peak mit Solarladen muss die Ladestation stoppen.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$test->runCase('Fahrzeugstrom wird nach stabiler Unterschreitung begrenzt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($stromerkennung, $anforderungSenden): void {
|
||||||
|
IPS_RequestAction($stromerkennung, 'Aktiv', true);
|
||||||
|
IPS_RequestAction($stromerkennung, 'Solarladen', true);
|
||||||
|
$anforderungSenden($stromerkennung, 13680);
|
||||||
|
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
|
||||||
|
IPS_Sleep(2100);
|
||||||
|
IPS_RequestAction($stromerkennung, 'Statuszyklus', false);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
18,
|
||||||
|
GetValue($test->objectByIdent('FahrzeugMaximalstrom', $stromerkennung))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
'[0,4104,4788,5472,6156,6840,7524,8208,8892,9576,10260,10944,11628,12312]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stromerkennung))
|
||||||
|
);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stromerkennung)),
|
||||||
|
'amp=18'
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
$test->runCase('Neue Ladestation wird erst nach Freigabe mit Maximalstrom erkannt', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($phasenerkennung, $anforderungSenden): void {
|
||||||
|
$stationId = $phasenerkennung;
|
||||||
|
$test->assertSame(
|
||||||
|
0,
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertTrue(!str_contains(
|
||||||
|
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||||
|
'amp=16'
|
||||||
|
), 'Eine inaktive Station darf keine Maximalstrom-Probe starten.');
|
||||||
|
|
||||||
|
IPS_RequestAction($stationId, 'Aktiv', true);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
0,
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertTrue(str_contains(
|
||||||
|
(string) GetValue($test->objectByIdent('LetzterGeraetebefehl', $stationId)),
|
||||||
|
'amp=16'
|
||||||
|
), 'Nach der Freigabe muss die Maximalstrom-Probe starten.');
|
||||||
|
|
||||||
|
IPS_Sleep(2100);
|
||||||
|
IPS_RequestAction($stationId, 'Statuszyklus', false);
|
||||||
|
$anforderungSenden($stationId, 3680);
|
||||||
|
IPS_RequestAction($stationId, 'Statuszyklus', false);
|
||||||
|
|
||||||
|
$test->assertSame(
|
||||||
|
1,
|
||||||
|
GetValue($test->objectByIdent('Phasenzahl', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
false,
|
||||||
|
GetValue($test->objectByIdent('FahrzeugGeladen', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
true,
|
||||||
|
GetValue($test->objectByIdent('SollwertGueltig', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
3680,
|
||||||
|
GetValue($test->objectByIdent('Sollleistung', $stationId))
|
||||||
|
);
|
||||||
|
$test->assertSame(
|
||||||
|
'[0,1380,1610,1840,2070,2300,2530,2760,2990,3220,3450,3680]',
|
||||||
|
(string) GetValue($test->objectByIdent('LeistungsangebotDiagnose', $stationId))
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,6 +268,33 @@ return static function (TestContext $test): void {
|
|||||||
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
|
'{BA90FDB3-C203-8F26-B4D6-F3DB99905724}',
|
||||||
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID']
|
IPS_GetInstance($energieflussID)['ModuleInfo']['ModuleID']
|
||||||
);
|
);
|
||||||
|
$knoten = json_decode(
|
||||||
|
(string) IPS_GetProperty($energieflussID, 'Config'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$namen = array_column($knoten, 'Name');
|
||||||
|
foreach (['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'] as $name) {
|
||||||
|
$test->assertTrue(in_array($name, $namen, true));
|
||||||
|
}
|
||||||
|
foreach ($knoten as $knotenEintrag) {
|
||||||
|
if (in_array(
|
||||||
|
$knotenEintrag['Name'],
|
||||||
|
['Verbraucher 1-Stufig', 'Wassererwärmer', 'Pufferspeicher'],
|
||||||
|
true
|
||||||
|
)) {
|
||||||
|
$test->assertTrue((int) $knotenEintrag['InfoVariableID'] > 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pvFlussID = $test->objectByIdent('EnergieflussPV', $managerId);
|
||||||
|
$test->assertSame(4200.0, (float) GetValue($pvFlussID));
|
||||||
|
$test->assertSame('~Power', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||||
|
|
||||||
|
IPS_SetProperty($managerId, 'EnergieflussLeistungseinheit', 1);
|
||||||
|
IPS_ApplyChanges($managerId);
|
||||||
|
$test->assertSame(4.2, (float) GetValue($pvFlussID));
|
||||||
|
$test->assertSame('Enelix.Power.kW', IPS_GetVariable($pvFlussID)['VariableProfile']);
|
||||||
|
|
||||||
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
|
IPS_RequestAction($managerId, 'FormEnergieflussAnzeigen', false);
|
||||||
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
$test->assertSame(true, (bool) IPS_GetObject($energieflussID)['ObjectIsHidden']);
|
||||||
@@ -416,12 +443,10 @@ return static function (TestContext $test): void {
|
|||||||
|
|
||||||
SetValue($temperaturId, 53.0);
|
SetValue($temperaturId, 53.0);
|
||||||
$test->waitUntil(static function () use (
|
$test->waitUntil(static function () use (
|
||||||
$managerId,
|
|
||||||
$warmwasserId,
|
$warmwasserId,
|
||||||
$verbraucherstatus,
|
$verbraucherstatus,
|
||||||
$zustandswert
|
$zustandswert
|
||||||
): bool {
|
): bool {
|
||||||
IPS_RequestAction($managerId, 'Regeln', false);
|
|
||||||
$wert = $zustandswert(
|
$wert = $zustandswert(
|
||||||
$verbraucherstatus(),
|
$verbraucherstatus(),
|
||||||
$warmwasserId,
|
$warmwasserId,
|
||||||
@@ -444,4 +469,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
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ return static function (TestContext $test): void {
|
|||||||
'Wiederholsperre' => 3,
|
'Wiederholsperre' => 3,
|
||||||
'Mindestlaufzeit' => 5,
|
'Mindestlaufzeit' => 5,
|
||||||
'Mindestsperrzeit' => 0,
|
'Mindestsperrzeit' => 0,
|
||||||
'MaximaleSperrzeit' => 2,
|
'MaximaleSperrzeit' => 3,
|
||||||
'MaximaleSperrzeit24h' => 60,
|
'MaximaleSperrzeit24h' => 60,
|
||||||
'VorgabeTimeout' => 30,
|
'VorgabeTimeout' => 30,
|
||||||
'DiagnosevariablenAnzeigen' => true,
|
'DiagnosevariablenAnzeigen' => true,
|
||||||
@@ -136,13 +136,16 @@ return static function (TestContext $test): void {
|
|||||||
) use ($waermepumpe, $kontakt1, $kontakt2, $senden): void {
|
) use ($waermepumpe, $kontakt1, $kontakt2, $senden): void {
|
||||||
$senden(null, 'Peak');
|
$senden(null, 'Peak');
|
||||||
$senden(0, 'Peak');
|
$senden(0, 'Peak');
|
||||||
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === true);
|
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === true, 2500);
|
||||||
$test->assertSame(false, GetValue($kontakt2));
|
$test->assertSame(false, GetValue($kontakt2));
|
||||||
$test->assertSame(true, GetValue(
|
$test->assertSame(true, GetValue(
|
||||||
$test->objectByIdent('SperreAktiv', $waermepumpe)
|
$test->objectByIdent('SperreAktiv', $waermepumpe)
|
||||||
));
|
));
|
||||||
|
|
||||||
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === false, 2500);
|
$test->waitUntil(static fn (): bool => GetValue($kontakt1) === false, 3500);
|
||||||
|
$test->waitUntil(static fn (): bool => GetValue(
|
||||||
|
$test->objectByIdent('SperrerholungAktiv', $waermepumpe)
|
||||||
|
) === true);
|
||||||
$test->assertSame(true, GetValue(
|
$test->assertSame(true, GetValue(
|
||||||
$test->objectByIdent('SperrerholungAktiv', $waermepumpe)
|
$test->objectByIdent('SperrerholungAktiv', $waermepumpe)
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ return static function (TestContext $test): void {
|
|||||||
'LastwechselSperrzeit' => 1,
|
'LastwechselSperrzeit' => 1,
|
||||||
'Hysterese' => 5.0,
|
'Hysterese' => 5.0,
|
||||||
'TemperaturMaxAlter' => 120,
|
'TemperaturMaxAlter' => 120,
|
||||||
|
'TemperaturUntergrenze' => 0.0,
|
||||||
|
'TemperaturObergrenze' => 100.0,
|
||||||
'LegionellenfunktionAktiv' => true,
|
'LegionellenfunktionAktiv' => true,
|
||||||
'LegionellenMinimalintervallTage' => 4,
|
'LegionellenMinimalintervallTage' => 4,
|
||||||
'LegionellenMaximalintervallTage' => 7,
|
'LegionellenMaximalintervallTage' => 7,
|
||||||
@@ -90,6 +92,24 @@ return static function (TestContext $test): void {
|
|||||||
IPS_RequestAction($managerId, 'Regeln', true);
|
IPS_RequestAction($managerId, 'Regeln', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$test->runCase('Temperaturgrenzen melden Unter- und Uebertemperatur', static function (
|
||||||
|
TestContext $test
|
||||||
|
) use ($verbraucherId, $temperaturSetzen): void {
|
||||||
|
$stoertextId = $test->objectByIdent('Stoertext', $verbraucherId);
|
||||||
|
|
||||||
|
$temperaturSetzen(100.0);
|
||||||
|
$test->assertTrue(!str_contains((string) GetValue($stoertextId), 'Uebertemperatur'));
|
||||||
|
$temperaturSetzen(100.1);
|
||||||
|
$test->assertTrue(str_contains((string) GetValue($stoertextId), 'Uebertemperatur'));
|
||||||
|
|
||||||
|
$temperaturSetzen(0.0);
|
||||||
|
$test->assertTrue(!str_contains((string) GetValue($stoertextId), 'Untertemperatur'));
|
||||||
|
$temperaturSetzen(-0.1);
|
||||||
|
$test->assertTrue(str_contains((string) GetValue($stoertextId), 'Untertemperatur'));
|
||||||
|
|
||||||
|
$temperaturSetzen(52.0);
|
||||||
|
});
|
||||||
|
|
||||||
if ($lizenzcode === '') {
|
if ($lizenzcode === '') {
|
||||||
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
|
$test->runCase('Manager bleibt ohne Lizenz sicher gesperrt', static function (
|
||||||
TestContext $test
|
TestContext $test
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase
|
|||||||
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
self::assertMatchesRegularExpression('/^\{[0-9A-F-]{36}\}$/', $metadaten['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAlle19PropertiesSindRegistriertUndImFormularDokumentiert(): void
|
public function testAlle21PropertiesSindRegistriertUndImFormularDokumentiert(): void
|
||||||
{
|
{
|
||||||
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
$basis = (string) file_get_contents(__DIR__ . '/../libs/VerbraucherBasisTrait.php');
|
||||||
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
$modul = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer);
|
preg_match_all('/RegisterProperty(?:Integer|Float|Boolean|String)\(\'([^\']+)\'/', $basis . $modul, $treffer);
|
||||||
$properties = array_values(array_unique($treffer[1]));
|
$properties = array_values(array_unique($treffer[1]));
|
||||||
|
|
||||||
self::assertCount(19, $properties);
|
self::assertCount(21, $properties);
|
||||||
|
|
||||||
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
$formular = (string) file_get_contents(self::MODULPFAD . 'form.json');
|
||||||
foreach ($properties as $property) {
|
foreach ($properties as $property) {
|
||||||
@@ -136,6 +136,38 @@ final class WarmwassererwaermerModulstrukturTest extends TestCase
|
|||||||
self::assertStringContainsString('Mindesttemperatur < Maximaltemperatur', $inhalt);
|
self::assertStringContainsString('Mindesttemperatur < Maximaltemperatur', $inhalt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testTemperaturstoergrenzenSindKonfigurierbarUndVonSollwertenGetrennt(): void
|
||||||
|
{
|
||||||
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyFloat('TemperaturUntergrenze', 0.0)",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString(
|
||||||
|
"RegisterPropertyFloat('TemperaturObergrenze', 100.0)",
|
||||||
|
$inhalt
|
||||||
|
);
|
||||||
|
self::assertStringContainsString("ReadPropertyFloat('TemperaturUntergrenze')", $inhalt);
|
||||||
|
self::assertStringContainsString("ReadPropertyFloat('TemperaturObergrenze')", $inhalt);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Untertemperatur'", $inhalt);
|
||||||
|
self::assertStringContainsString("'Kennung' => 'Uebertemperatur'", $inhalt);
|
||||||
|
|
||||||
|
$formular = json_decode(
|
||||||
|
(string) file_get_contents(self::MODULPFAD . 'form.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$erweitert = array_values(array_filter(
|
||||||
|
$formular['elements'],
|
||||||
|
static fn (array $element): bool =>
|
||||||
|
$element['caption'] === 'Erweiterte Speichereinstellungen'
|
||||||
|
));
|
||||||
|
self::assertCount(1, $erweitert);
|
||||||
|
self::assertContains('TemperaturUntergrenze', array_column($erweitert[0]['items'], 'name'));
|
||||||
|
self::assertContains('TemperaturObergrenze', array_column($erweitert[0]['items'], 'name'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testSchaltfolgeIstBreakBeforeMake(): void
|
public function testSchaltfolgeIstBreakBeforeMake(): void
|
||||||
{
|
{
|
||||||
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
$inhalt = (string) file_get_contents(self::MODULPFAD . 'module.php');
|
||||||
|
|||||||
Reference in New Issue
Block a user