diff --git a/Ladestation/README.md b/Ladestation/README.md index 5262b24..8c1187d 100644 --- a/Ladestation/README.md +++ b/Ladestation/README.md @@ -1,91 +1,44 @@ # Ladestation -Das Modul steuert verschiedene Ladestationstypen. +Das Modul steuert die Ladestationstypen 1 bis 6 mit der ursprünglichen, +gemeinsamen Strom-, Phasen- und Ladeende-Erkennung. -Das Konfigurationsformular zeigt die Verbindungsdaten abhängig vom gewählten -Ladestationstyp an. Allgemeine Regel- und Zeitparameter bleiben immer sichtbar. +## Gemeinsame Pending-Counter-Logik + +Die Erkennung arbeitet bei allen Stationstypen gleich: + +1. Bei einem neu verbundenen Fahrzeug wird zunächst der konfigurierte + Maximalstrom an die Station gesendet. +2. Nach 60 Sekunden wird anhand der tatsächlichen Ladeleistung zwischen + einphasigem und dreiphasigem Laden unterschieden. +3. Der erreichbare Maximalstrom wird wie ursprünglich aus der Istleistung plus + 2,5 A Reserve berechnet und durch `Max_Current_abs` begrenzt. +4. Im laufenden Betrieb vergleicht der `Pending_Counter` Soll- und Istleistung. + Bleibt die Abweichung 90 Sekunden bestehen, wird der Maximalstrom erneut + berechnet. +5. Fällt der berechnete Maximalstrom unter 6 A, wird + `Auto fertig geladen` gesetzt. + +Eine Sollvorgabe von 0 W setzt den `Pending_Counter` zurück. Die vorhandenen +Leistungsschritte, Mindestlaufzeiten und die Solar-/Peak-Umschaltung entsprechen +der Vorversion. ## Easee -Die Ladestationstypen 5 und 6 verwenden das im Konfigurationsformular -ausgewählte `Easee SignalR Gateway`. Die Auswahl wird nur bei diesen beiden -Stationstypen angezeigt. +Die Typen 5 und 6 verwenden dieselbe Logik wie die Typen 1 bis 4. Lediglich die +Kommunikation erfolgt über das `Easee SignalR Gateway`: -- Typ 5: Easee mit zusätzlicher eCarUp-Benutzerprüfung für Solarladen -- Typ 6: Easee ohne eCarUp-Benutzerprüfung +- Fahrzeugstatus und Ladeleistung werden aus den Gateway-Observations gelesen. +- Stromvorgaben werden als `SetDynamicChargerCurrent` an das Gateway gesendet. +- Es erfolgt keine interne Anmeldung an der Easee-REST-API. -Die Easee-Zugangsdaten werden ausschließlich im Gateway eingetragen. In der -Ladestationsinstanz muss die Easee-Seriennummer hinterlegt sein. - -Das Gateway liefert: - -- Observation 47: maximaler Ladestrom der Station -- Observation 104: Stromgrenze des angeschlossenen Typ-2-Kabels -- Observation 109: Fahrzeug-/Ladestatus -- Observation 110: freigegebene Ausgangsphase -- Observation 120: aktuelle Gesamtleistung in kW -- Observations 182 bis 185: gemessene Leiterströme - -Die Fahrzeugphasen werden anhand der tatsächlich gemessenen Leiterströme -erkannt; alte Phasenwerte werden beim Trennen oder Neuverbinden verworfen. -Während einer Ladepause mit 0 W bleibt die zuletzt erkannte Phasenanzahl -erhalten. Sie ändert sich erst bei einer eindeutigen Strommessung oder beim -Abstecken des Fahrzeugs. Solange noch kein eindeutiger API-Phasenwert vorliegt, -wird nur ein bilanzierter 6-A-Prüfschritt angeboten. Nach 60 Sekunden kann die -Phase ersatzweise aus Soll- und Istleistung bestimmt werden. - -Stromvorgaben von 0 bis 32 A werden ebenfalls über das Gateway gesendet. - -## Andere Ladestationstypen - -Die Typen 1 bis 4 benötigen kein Easee-Gateway. - -### Go-e - -Die Typen 1 und 2 erkennen ein verbundenes Fahrzeug direkt über den -Go-e-Fahrzeugstatus. Die tatsächlich verwendeten Phasen werden aus den drei -gemessenen Strömen des `nrg`-Arrays bestimmt. Der maximale Ladestrom wird durch -`Max_Current_abs`, `ama`, `cbl` und einen gegebenenfalls vorhandenen -16-A-Adapter begrenzt. - -### Smart-Me Pico - -Typ 3 erkennt ein verbundenes Fahrzeug über den Pico-Ladestatus. Die -Phasenerkennung verwendet `CurrentL1`, `CurrentL2` und `CurrentL3` des -Smart-Me-Geräte-Endpunkts. Der maximale Stationsstrom wird direkt aus den -Pico-Ladedaten übernommen. - -## Gemeinsames Soll-/Ist-Modell - -Der konfigurierte Maximalstrom ist bei allen Typen die absolute Obergrenze. -Statische, zuverlässige API-Grenzen dürfen sie reduzieren. Dynamische -Sollwerte der Station werden nicht als Fahrzeuggrenze verwendet. - -Bei jedem stabil zugewiesenen Leistungsschritt wird der Sollstrom mit dem -Iststrom verglichen. Bleibt der Sollstrom 90 Sekunden lang größer als der -Iststrom mal `1.11`, wird der erreichbare Maximalstrom aus `Iststrom * 1.11` -gelernt und `PowerSteps` noch im selben Zyklus neu aufgebaut. Der Lernwert -bleibt bis zum Ausstecken, einem neuen Fahrzeug oder einem Phasenwechsel aktiv. - -Fällt der berechnete Wert unter 6 A, wird `Auto fertig geladen` gesetzt und -nur `[0]` angeboten. Bei Easee gilt das nur bei verbundenem Gateway und -Fahrzeugstatus 4 oder 6: Das Modul muss dabei trotz positiver Sollvorgabe -90 Sekunden lang einen Iststrom messen, der mit Faktor `1.11` unter 6 A -bleibt. Status 4 oder 6 allein wird nicht als Ladeende gewertet. Eine -Sollvorgabe von 0 W setzt die Zähler zurück und kann weder eine -Strombegrenzung noch ein Ladeende auslösen. Das bestätigte Ladeende bleibt bis -zum Ausstecken gespeichert. - -Beim Update bestehender Instanzen wird die frühere Eigenschaft `Token_Easee` -aus Kompatibilitätsgründen intern beibehalten, aber nicht mehr verwendet oder -im Formular angezeigt. Auch der frühere Token-Timer wird deaktiviert. Die -vorhandene Modul- und Instanzidentität bleibt erhalten. +Typ 5 behält die eCarUp-Benutzerprüfung für die automatische Umschaltung von +`Solarladen`. Typ 6 verwendet keine Benutzerprüfung. ## Einrichtung 1. Eine Instanz `Easee SignalR Gateway` anlegen. 2. Easee-Benutzername und Passwort im Gateway eintragen. 3. In `Ladestation` Typ 5 oder 6 sowie die Easee-Seriennummer wählen. -4. Im Feld `Easee SignalR Gateway` die gewünschte Gateway-Instanz auswählen. +4. Im Feld `Easee SignalR Gateway` die Gateway-Instanz auswählen. -Mehrere Easee-Ladestationen desselben Kontos verwenden gemeinsam ein Gateway. diff --git a/Ladestation/form.json b/Ladestation/form.json index 23200c3..10fa497 100644 --- a/Ladestation/form.json +++ b/Ladestation/form.json @@ -119,3 +119,5 @@ } ] } + + diff --git a/Ladestation/module.json b/Ladestation/module.json index 8d0cf52..ef6a788 100644 --- a/Ladestation/module.json +++ b/Ladestation/module.json @@ -10,3 +10,5 @@ "prefix": "GEF", "url": "https://git.belevo.ch/dh/Enelix/src/branch/main/Ladestation" } + + diff --git a/Ladestation/module.php b/Ladestation/module.php index 96d7217..167027e 100644 --- a/Ladestation/module.php +++ b/Ladestation/module.php @@ -5,10 +5,6 @@ class Ladestation extends IPSModule private const EASEE_GATEWAY_MODULE_ID = "{7A1F1D4B-3E7D-4A3B-9E54-61C8C3B7F201}"; private const EASEE_GATEWAY_REQUEST_ID = "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}"; private const EASEE_GATEWAY_EVENT_ID = "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}"; - private const ADAPTIVE_MODEL_VERSION = "5"; - private const ADAPTIVE_MISMATCH_FACTOR = 1.11; - private const ADAPTIVE_CONFIRMATION_SECONDS = 90; - private const PHASE_CONFIRMATION_SECONDS = 60; public function Create() { @@ -37,7 +33,6 @@ class Ladestation extends IPSModule // Ladestationspezifische Variabeln $this->RegisterVariableFloat("Max_Current","Maximaler Ladestrom","", 0); IPS_SetHidden($this->GetIDForIdent("Max_Current"), true); - $this->SetValue("Max_Current", (float)$this->ReadPropertyInteger("Max_Current_abs")); $this->RegisterVariableBoolean("Is_1_ph", "Auto lädt 1-Phasig", "~Switch", false); $this->RegisterVariableBoolean("Car_detected", "Auto erkannt","~Switch", false); $this->RegisterVariableInteger("Pending_Counter", "Pending_Counter", "", 0); @@ -107,19 +102,6 @@ class Ladestation extends IPSModule ); IPS_SetHidden($this->GetIDForIdent("Easee_Gateway_Connected"), true); $this->SetBuffer("EaseeGatewayState", "{}"); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetBuffer("AdaptiveMeasurementTimestamp", "0"); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $this->SetBuffer( - "AdaptiveStationType", - (string)$this->ReadPropertyInteger("Ladestation") - ); - $this->SetBuffer("AdaptiveModelVersion", self::ADAPTIVE_MODEL_VERSION); // Kompatibilität mit bestehenden Instanzen der ursprünglichen Version. // Der Inhalt wird nicht mehr verwendet; die Easee-Anmeldung übernimmt @@ -289,46 +271,6 @@ class Ladestation extends IPSModule // Einen noch vorhandenen Token-Timer aus Altinstanzen sicher stilllegen. $this->SetTimerInterval("Timer_Refresh_Token", 0); $stationType = $this->ReadPropertyInteger("Ladestation"); - - // Einmalige Migration aus den bisherigen stationsspezifischen - // Lernalgorithmen in das gemeinsame Soll-/Ist-Modell. - if ( - $this->GetBuffer("AdaptiveModelVersion") !== self::ADAPTIVE_MODEL_VERSION || - (int)$this->GetBuffer("AdaptiveStationType") !== $stationType - ) { - $this->SetValue("Car_detected", false); - $this->SetValue("Car_is_full", false); - $this->SetValue("Is_1_ph", false); - $this->SetValue("Pending_Counter", 0); - $this->SetValue( - "Max_Current", - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetBuffer("AdaptiveMeasurementTimestamp", "0"); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $this->SetBuffer("AdaptiveStationType", (string)$stationType); - $this->SetBuffer("SmartMeChargingEnabled", "0"); - $this->SetBuffer("SmartMeMinStationCurrent", "6"); - $this->SetBuffer("AdaptiveModelVersion", self::ADAPTIVE_MODEL_VERSION); - } - - if (!$this->GetValue("Car_detected")) { - $configuredMaxCurrent = (float)$this->ReadPropertyInteger( - "Max_Current_abs" - ); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$configuredMaxCurrent - ); - $this->SetValue("Max_Current", $configuredMaxCurrent); - } - $usesEaseeGateway = $this->UsesEaseeGateway($stationType); IPS_SetHidden( $this->GetIDForIdent("Easee_Gateway_Connected"), @@ -526,280 +468,11 @@ class Ladestation extends IPSModule return $stationType === 5 || $stationType === 6; } - private function UsesAdaptiveCurrentDetection(int $stationType): bool + private function UsesDirectStationDetection(int $stationType): bool { - return in_array($stationType, [1, 2, 3, 4, 5, 6], true); - } - - private function CanUseAdaptiveFullDetection(int $stationType): bool - { - if (!$this->UsesEaseeGateway($stationType)) { - return true; - } - - // Easee-Status 4 bedeutet "pausiert/gestoppt", Status 6 bedeutet, - // dass die Station auf eine Stromaufnahme des Fahrzeugs wartet. Beide - // sind nur zusammen mit positivem Soll und einer 90 Sekunden lang - // bestätigten, zu geringen Stromaufnahme ein Ladeende-Kandidat. - return $this->GetValue("Easee_Gateway_Connected") && - in_array((int)$this->GetValue("Fahrzeugstatus"), [4, 6], true); - } - - private function SetAdaptiveReferenceMaxCurrent(float $apiMaxCurrent): float - { - $configuredMaxCurrent = max( - 0.0, - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - $referenceMaxCurrent = $configuredMaxCurrent; - - // Nur plausible statische Stations-/Kabelgrenzen aus einer API dürfen - // die konfigurierte Obergrenze reduzieren. - if ($apiMaxCurrent >= 6.0) { - $referenceMaxCurrent = min($configuredMaxCurrent, $apiMaxCurrent); - } - - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$referenceMaxCurrent - ); - return $referenceMaxCurrent; - } - - private function GetAdaptiveReferenceMaxCurrent(): float - { - $referenceMaxCurrent = (float)$this->GetBuffer( - "AdaptiveReferenceMaxCurrent" - ); - if ($referenceMaxCurrent <= 0) { - $referenceMaxCurrent = max( - 0.0, - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - } - - return min( - $referenceMaxCurrent, - max(0.0, (float)$this->ReadPropertyInteger("Max_Current_abs")) - ); - } - - private function ResetAdaptiveLearning(float $referenceMaxCurrent) - { - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetValue("Pending_Counter", 0); - $this->SetValue("Max_Current", $referenceMaxCurrent); - } - - private function StoreAdaptiveMeasurement(int $timestamp = 0) - { - $this->SetBuffer( - "AdaptiveMeasurementTimestamp", - (string)($timestamp > 0 ? $timestamp : time()) - ); - } - - private function HasFreshAdaptiveMeasurement(int $stationType): bool - { - // Der Dummy besitzt keine API-Zeitmarke und verwendet weiterhin den - // direkt gesetzten Istwert. - if ($stationType === 4) { - return true; - } - - $timestamp = (int)$this->GetBuffer("AdaptiveMeasurementTimestamp"); - $maximumAge = max( - 30, - 3 * max(1, $this->ReadPropertyInteger("Interval")) - ); - return $timestamp > 0 && (time() - $timestamp) <= $maximumAge; - } - - private function IsAdaptiveChargingEnabled(int $stationType): bool - { - if ($stationType === 5) { - return $this->ReadPropertyString("Username") === - $this->GetValue("Letzer_User"); - } - - // Die verbindliche Freigabe ist die positive Sollleistung des Managers. - // Statusfelder der Station können beim echten Ladeende bereits 0 sein - // und dürfen deshalb die Ladeende-Erkennung nicht blockieren. - return true; - } - - private function UpdateFallbackPhaseDetection( - int $stationType, - bool $ladebereit - ): bool { - if ( - $stationType === 4 || - $this->GetBuffer("AdaptivePhaseKnown") === "1" - ) { - $this->SetBuffer("AdaptivePhaseCounter", "0"); - return false; - } - - $requestedPower = max( - 0.0, - (float)$this->GetValue("Aktuelle_Leistung") - ); - $requestedCurrent = $requestedPower / (1.71 * 400.0); - $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && - $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $this->GetValue("Idle") && - $requestedCurrent >= 5.5 && - $requestedCurrent <= 6.5 && - $this->IsAdaptiveChargingEnabled($stationType) && - $this->HasFreshAdaptiveMeasurement($stationType); - - if (!$canEvaluate) { - $this->SetBuffer("AdaptivePhaseCounter", "0"); - return false; - } - - $counter = (int)$this->GetBuffer("AdaptivePhaseCounter") + 1; - $this->SetBuffer("AdaptivePhaseCounter", (string)$counter); - $requiredCycles = max( - 1, - (int)ceil( - self::PHASE_CONFIRMATION_SECONDS / - max(1, $this->ReadPropertyInteger("Interval")) - ) - ); - if ($counter < $requiredCycles) { - return false; - } - - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $actualPower = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ); - // Ohne messbaren Strom ist keine Phasenerkennung möglich. Vor allem - // darf eine Ladepause die zuletzt bekannte Phase nicht auf 1-phasig - // umschalten. Die Ladeende-Erkennung läuft separat weiter. - if ($actualPower < 100.0) { - return false; - } - - $onePhaseExpectedPower = $requestedCurrent * 230.0; - $threePhaseExpectedPower = $requestedCurrent * 1.71 * 400.0; - $isOnePhase = $actualPower < - (($onePhaseExpectedPower + $threePhaseExpectedPower) / 2.0); - - $this->SetValue("Is_1_ph", $isOnePhase); - $this->SetBuffer("AdaptivePhaseKnown", "1"); - $this->ResetAdaptiveLearning($this->GetAdaptiveReferenceMaxCurrent()); - - $phaseDivisor = $isOnePhase ? 230.0 : 1.71 * 400.0; - $this->SetValue( - "Power", - (int)round($this->GetValue("Mindestaldestrom") * $phaseDivisor) - ); - $detectedCurrent = self::ADAPTIVE_MISMATCH_FACTOR * - ($actualPower / $phaseDivisor); - if ( - $detectedCurrent < 6.0 && - $this->CanUseAdaptiveFullDetection($stationType) - ) { - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$detectedCurrent - ); - $this->SetValue("Max_Current", $detectedCurrent); - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - } - - return true; - } - - private function UpdateAdaptiveCurrentLimit( - int $stationType, - bool $ladebereit - ): bool { - $requestedPower = max( - 0.0, - (float)$this->GetValue("Aktuelle_Leistung") - ); - $actualPower = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ); - $hasKnownPhase = $stationType === 4 || - $this->GetBuffer("AdaptivePhaseKnown") === "1"; - - $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && - $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $this->GetValue("Idle") && - ($hasKnownPhase || $actualPower < 100.0) && - $requestedPower > 0 && - $this->IsAdaptiveChargingEnabled($stationType) && - $this->HasFreshAdaptiveMeasurement($stationType); - - if (!$canEvaluate) { - $this->SetValue("Pending_Counter", 0); - return false; - } - - $phaseDivisor = $this->GetValue("Is_1_ph") ? 230.0 : 1.71 * 400.0; - $requestedCurrent = $requestedPower / $phaseDivisor; - $actualCurrent = $actualPower / $phaseDivisor; - - if ( - $requestedCurrent <= - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ) { - $this->SetValue("Pending_Counter", 0); - return false; - } - - $counter = $this->GetValue("Pending_Counter") + 1; - $this->SetValue("Pending_Counter", $counter); - $requiredCycles = max( - 1, - (int)ceil( - self::ADAPTIVE_CONFIRMATION_SECONDS / - max(1, $this->ReadPropertyInteger("Interval")) - ) - ); - - if ($counter < $requiredCycles) { - return false; - } - - $this->SetValue("Pending_Counter", 0); - $learnedMaxCurrent = min( - $this->GetAdaptiveReferenceMaxCurrent(), - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ); - - if ($learnedMaxCurrent < 6.0) { - if (!$this->CanUseAdaptiveFullDetection($stationType)) { - return false; - } - - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue("Max_Current", $learnedMaxCurrent); - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - return true; - } - - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue("Max_Current", $learnedMaxCurrent); - return true; + // Alle Stationstypen verwenden wieder die gemeinsame, ursprüngliche + // Strom-/Phasenerkennung mit Pending_Counter. + return false; } private function ApplyEaseeGatewayResponse($response) @@ -821,7 +494,7 @@ class Ladestation extends IPSModule } $updated = (int)($response["state"]["updated"] ?? time()); - foreach ([47, 104, 109, 110, 120, 182, 183, 184, 185] as $observationID) { + foreach ([47, 109, 110, 120, 182, 183, 184, 185] as $observationID) { $key = (string)$observationID; if (array_key_exists($key, $response["state"])) { $this->StoreEaseeObservation( @@ -835,7 +508,7 @@ class Ladestation extends IPSModule private function StoreEaseeObservation(int $observationID, $value, int $timestamp) { - if (!in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true)) { + if (!in_array($observationID, [47, 109, 110, 120, 182, 183, 184, 185], true)) { return; } @@ -861,7 +534,6 @@ class Ladestation extends IPSModule $this->SetValue("Fahrzeugstatus", (int)$value); } elseif ($observationID === 120) { $this->SetValue("Ladeleistung_Effektiv", round((float)$value * 1000)); - $this->StoreAdaptiveMeasurement($timestamp); } } @@ -889,13 +561,18 @@ class Ladestation extends IPSModule private function DetectEaseeCar(int $carType): bool { + $wasDetected = $this->GetValue("Car_detected"); + $wasOnePhase = $this->GetValue("Is_1_ph"); $carDetected = $this->Get_Car_Status($carType); $state = json_decode($this->GetBuffer("EaseeGatewayState"), true); if (!is_array($state)) { $state = []; } - $isOnePhase = $this->DetectEaseePhases($state); + $this->SetValue("Pending_Counter", 0); + + $this->DetectEaseePhases($state); + $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); // Observation 47 ist der nichtflüchtige maximale Ladestrom der // Station. Die lokal konfigurierte Obergrenze bleibt zusätzlich aktiv. @@ -903,26 +580,39 @@ class Ladestation extends IPSModule if (array_key_exists("47", $state) && (float)$state["47"] > 0) { $maxCurrent = min($maxCurrent, (float)$state["47"]); } - // Observation 104 ist die vom Typ-2-Kabel signalisierte Stromgrenze. - if (array_key_exists("104", $state) && (float)$state["104"] >= 6) { - $maxCurrent = min($maxCurrent, (float)$state["104"]); - } + $this->SetValue("Max_Current", $maxCurrent); - $chargerOpMode = (int)($state["109"] ?? -1); - if (!$carDetected && $chargerOpMode !== 1) { - // Einen vorübergehenden Fehlerstatus nicht als Ausstecken werten. + if (!$carDetected) { $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); - $this->SetValue("Pending_Counter", 0); + + $chargerOpMode = (int)($state["109"] ?? -1); + if ($chargerOpMode === 1) { + $this->SetValue("Leistung_Delta", 0); + $this->ResetTimer(); + $this->ResetNullTimer(); + $this->SetValue("Is_1_ph", false); + $this->SetValue("Aktuelle_Leistung", 0); + $this->SetValue("IdleCounter", 0); + $this->SetValue("Idle", true); + } + return false; } - return $this->ApplyDirectStationDetection( - $carDetected, - false, - $isOnePhase, - $maxCurrent - ); + $this->SetValue("Car_detected", true); + $this->SetValue("Car_is_full", (int)($state["109"] ?? 0) === 4); + + if (!$wasDetected || $phaseChanged) { + if ($this->GetValue("Is_1_ph")) { + $power = $this->GetValue("Mindestaldestrom") * 230; + } else { + $power = $this->GetValue("Mindestaldestrom") * 400 * 1.71; + } + $this->SetValue("Power", $power); + } + + return true; } private function DetectEaseePhases(array $state) @@ -935,7 +625,7 @@ class Ladestation extends IPSModule array_key_exists("120", $state) && abs((float)$state["120"]) < 0.1 ) { - return null; + return; } $activePhaseConductors = 0; @@ -946,25 +636,24 @@ class Ladestation extends IPSModule } if ($activePhaseConductors === 0) { - return null; + return; } // Ein explizit einphasiger Easee-Ausgang ist auch in IT-Netzen // einphasig, obwohl dort Strom auf zwei Leitern gemessen wird. if ($outputPhase >= 10 && $outputPhase <= 15) { - return true; + $this->SetValue("Is_1_ph", true); + return; } // Bei einem dreiphasig freigegebenen Ausgang kann ein einphasiges Auto // trotzdem nur auf einem Leiter Strom beziehen. Observation 110 allein // kann diesen Fahrzeugwechsel deshalb nicht erkennen. if ($activePhaseConductors === 1) { - return true; + $this->SetValue("Is_1_ph", true); } elseif ($activePhaseConductors >= 2) { - return false; + $this->SetValue("Is_1_ph", false); } - - return null; } private function ReadJsonEndpoint(string $url, bool $useAuthentication = false) @@ -1024,19 +713,19 @@ class Ladestation extends IPSModule bool $carDetected, bool $carIsFull, $isOnePhase, - float $maxCurrent + float $maxCurrent, + bool $resetPendingCounter = true ): bool { $wasDetected = $this->GetValue("Car_detected"); $wasOnePhase = $this->GetValue("Is_1_ph"); - $wasFull = $this->GetValue("Car_is_full"); - $referenceMaxCurrent = $this->SetAdaptiveReferenceMaxCurrent( - $maxCurrent - ); + + if ($resetPendingCounter) { + $this->SetValue("Pending_Counter", 0); + } + $this->SetValue("Max_Current", $maxCurrent); if (!$carDetected) { - $this->ResetAdaptiveLearning($referenceMaxCurrent); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); + $this->SetValue("Pending_Counter", 0); $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); $this->SetValue("Leistung_Delta", 0); @@ -1051,36 +740,12 @@ class Ladestation extends IPSModule if (is_bool($isOnePhase)) { $this->SetValue("Is_1_ph", $isOnePhase); - $this->SetBuffer("AdaptivePhaseKnown", "1"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - } elseif (!$wasDetected) { - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - } - - $phaseChanged = $wasDetected && - is_bool($isOnePhase) && - $wasOnePhase !== $this->GetValue("Is_1_ph"); - if (!$wasDetected || $phaseChanged) { - $this->ResetAdaptiveLearning($referenceMaxCurrent); - } else { - $learnedMaxCurrent = (float)$this->GetBuffer( - "AdaptiveLearnedMaxCurrent" - ); - $this->SetValue( - "Max_Current", - $learnedMaxCurrent > 0 - ? min($referenceMaxCurrent, $learnedMaxCurrent) - : $referenceMaxCurrent - ); } $this->SetValue("Car_detected", true); - $this->SetValue( - "Car_is_full", - $carIsFull || ($wasDetected && !$phaseChanged && $wasFull) - ); + $this->SetValue("Car_is_full", $carIsFull); + $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); if (!$wasDetected || $phaseChanged) { if ($this->GetValue("Is_1_ph")) { $power = $this->GetValue("Mindestaldestrom") * 230; @@ -1106,10 +771,6 @@ class Ladestation extends IPSModule } } - if (!empty($data["adi"])) { - $maxCurrent = min($maxCurrent, 16.0); - } - if (isset($data["var"]) && is_numeric($data["var"])) { $variant = (int)$data["var"]; if ($variant === 11) { @@ -1128,14 +789,8 @@ class Ladestation extends IPSModule return (int)$data["alw"] === 1; } - if (array_key_exists("frc", $data)) { - $forceState = (int)$data["frc"]; - if ($forceState === 1) { - return false; - } - if ($forceState === 2) { - return true; - } + if ($stationType === 2 && array_key_exists("frc", $data)) { + return (int)$data["frc"] === 2; } if (array_key_exists("alw", $data)) { @@ -1145,6 +800,53 @@ class Ladestation extends IPSModule return $this->GetBuffer("GoEChargingEnabled") === "1"; } + private function DetectGoEFullCharge( + array $data, + int $stationType, + int $carState, + $chargingPowerWatts + ): bool { + $wasFull = $this->GetValue("Car_is_full"); + + // Ein einmal sicher erkanntes Ladeende bleibt bis zum Abstecken oder + // bis zu einer tatsächlich wieder aufgenommenen Ladung erhalten. + if ($wasFull) { + if ( + is_numeric($chargingPowerWatts) && + abs((float)$chargingPowerWatts) >= 100 + ) { + $this->SetValue("Pending_Counter", 0); + return false; + } + + return true; + } + + // car=4 alleine ist nicht ausreichend: Go-E meldet diesen Status auch, + // wenn die Regelung die Ladung absichtlich unterbricht. + if ( + $carState !== 4 || + !$this->IsGoEChargingEnabled($data, $stationType) || + !is_numeric($chargingPowerWatts) || + abs((float)$chargingPowerWatts) >= 100 + ) { + $this->SetValue("Pending_Counter", 0); + return false; + } + + $counter = $this->GetValue("Pending_Counter") + 1; + $this->SetValue("Pending_Counter", $counter); + + $interval = max(1, $this->ReadPropertyInteger("Interval")); + $requiredCycles = max(1, (int)ceil(90 / $interval)); + if ($counter < $requiredCycles) { + return false; + } + + $this->SetValue("Pending_Counter", 0); + return true; + } + private function DetectGoECar(int $stationType): bool { $path = $stationType === 1 ? "/mqtt?payload=" : "/api/status"; @@ -1162,10 +864,6 @@ class Ladestation extends IPSModule } $this->SetValue("Fahrzeugstatus", $carState); - $this->SetBuffer( - "GoEChargingEnabledFromStatus", - $this->IsGoEChargingEnabled($data, $stationType) ? "1" : "0" - ); $isOnePhase = null; $chargingPowerWatts = null; @@ -1187,27 +885,28 @@ class Ladestation extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); - $this->StoreAdaptiveMeasurement(); } } $carDetected = in_array($carState, [2, 3, 4, 5], true); - $stationMaxCurrent = $this->GetGoEMaxCurrent($data); + $carIsFull = $carDetected && $this->DetectGoEFullCharge( + $data, + $stationType, + $carState, + $chargingPowerWatts + ); return $this->ApplyDirectStationDetection( $carDetected, - false, + $carIsFull, $isOnePhase, - $stationMaxCurrent + $this->GetGoEMaxCurrent($data), + false ); } private function DetectSmartMeCar(): bool { - // Bei einem fehlgeschlagenen Abruf darf eine alte Freigabe keinesfalls - // zur Ladeende-Erkennung weiterverwendet werden. - $this->SetBuffer("SmartMeChargingEnabled", "0"); - $deviceID = trim($this->ReadPropertyString("ID")); if ($deviceID === "") { return $this->GetValue("Car_detected"); @@ -1235,51 +934,13 @@ class Ladestation extends IPSModule $chargingPower = $chargingData["ActiveChargingPower"] ?? $chargingData["activeChargingPower"] ?? null; - $chargingPowerWatts = null; if (is_numeric($chargingPower)) { - $chargingPowerWatts = (float)$chargingPower * 1000; $this->SetValue( "Ladeleistung_Effektiv", - round($chargingPowerWatts) + round((float)$chargingPower * 1000) ); - $this->StoreAdaptiveMeasurement(); } - // Die Pico-API unterscheidet zwischen der extern vorgegebenen - // Stromgrenze und der effektiv verfügbaren Stromgrenze. Nur wenn beide - // vorhandenen Grenzen mindestens dem Stationsminimum entsprechen, ist - // die Ladung wirklich freigegeben. Eine Regelungsunterbrechung mit 0 A - // darf nicht als abgeschlossenes Laden ausgewertet werden. - $minStationCurrent = $chargingData["MinStationCurrent"] ?? - $chargingData["minStationCurrent"] ?? 6; - $minStationCurrent = is_numeric($minStationCurrent) - ? max(1.0, (float)$minStationCurrent) - : 6.0; - $this->SetBuffer( - "SmartMeMinStationCurrent", - (string)$minStationCurrent - ); - - $maxDynamicCurrent = $chargingData["MaxDynamicCurrent"] ?? - $chargingData["maxDynamicCurrent"] ?? null; - $maxAllowedCurrent = $chargingData["MaxAllowedChargingCurrent"] ?? - $chargingData["maxAllowedChargingCurrent"] ?? null; - - $hasCurrentLimit = is_numeric($maxDynamicCurrent) || - is_numeric($maxAllowedCurrent); - $dynamicCurrentAllowsCharging = !is_numeric($maxDynamicCurrent) || - (float)$maxDynamicCurrent >= $minStationCurrent; - $allowedCurrentAllowsCharging = !is_numeric($maxAllowedCurrent) || - (float)$maxAllowedCurrent >= $minStationCurrent; - $chargingEnabled = $hasCurrentLimit - ? $dynamicCurrentAllowsCharging && $allowedCurrentAllowsCharging - : $this->GetValue("Aktuelle_Leistung") > 0; - - $this->SetBuffer( - "SmartMeChargingEnabled", - $chargingEnabled ? "1" : "0" - ); - $isOnePhase = null; $deviceData = $this->ReadJsonEndpoint( "https://api.smart-me.com/Devices/" . $deviceID, @@ -1298,6 +959,8 @@ class Ladestation extends IPSModule foreach ([ "MaxStationCurrent", "maxStationCurrent", + "MaxLoadmanagementGroupCurrent", + "maxLoadmanagementGroupCurrent", ] as $key) { if (isset($chargingData[$key]) && is_numeric($chargingData[$key])) { $candidate = (float)$chargingData[$key]; @@ -1307,10 +970,8 @@ class Ladestation extends IPSModule } } - $carDetected = in_array($stationState, [2, 3, 4, 6], true); - return $this->ApplyDirectStationDetection( - $carDetected, + in_array($stationState, [2, 3, 4, 6], true), false, $isOnePhase, $maxCurrent @@ -1338,19 +999,6 @@ class Ladestation extends IPSModule public function Detect_Car(int $carType) { - - if ($carType === 1 || $carType === 2) { - return $this->DetectGoECar($carType); - } - - if ($carType === 3) { - return $this->DetectSmartMeCar(); - } - - if ($this->UsesEaseeGateway($carType)) { - return $this->DetectEaseeCar($carType); - } - if($this->GetValue("Car_detected")==true){ if($this->Get_Car_Status($carType)==true){ $this->SetValue("Car_detected", true); @@ -1386,13 +1034,11 @@ class Ladestation extends IPSModule $this->SetValue("Pending_Counter", 0); if($this->GetValue("Ladeleistung_Effektiv")>7500){ $this->SetValue("Is_1_ph", false); - $this->SetBuffer("AdaptivePhaseKnown", "1"); $this->Calc_Max_Current(false); } else{ $this->SetValue("Is_1_ph", true); - $this->SetBuffer("AdaptivePhaseKnown", "1"); $this->Calc_Max_Current(true); } @@ -1629,26 +1275,19 @@ class Ladestation extends IPSModule public function Calc_Max_Current(bool $is_1_ph) { - $phaseDivisor = $is_1_ph ? 230.0 : 1.71 * 400.0; - $actualCurrent = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ) / $phaseDivisor; - $maxCurrent = min( - $this->GetAdaptiveReferenceMaxCurrent(), - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ); + $maxCurrent = 32; + if($is_1_ph){ - $this->SetBuffer("AdaptiveLearnedMaxCurrent", (string)$maxCurrent); - $this->SetValue("Max_Current", $maxCurrent); - - if ( - $maxCurrent < 6.0 && - $this->CanUseAdaptiveFullDetection( - $this->ReadPropertyInteger("Ladestation") - ) - ) { - $this->SetValue("Car_is_full", true); + $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / 230); } + else{ + + $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / (1.71*400)); + } + if($maxCurrent>$this->ReadPropertyInteger("Max_Current_abs")){ + $maxCurrent = $this->ReadPropertyInteger("Max_Current_abs"); + } + $this->SetValue("Max_Current", $maxCurrent); } public function Get_Current_From_Power(bool $is_1_ph, int $power) @@ -1778,101 +1417,93 @@ class Ladestation extends IPSModule } } - private function BuildPowerSteps( - bool $peak, - bool $ladebereit, - bool $solarladen - ): array { - if (!$ladebereit || $this->GetValue("Car_is_full")) { - return [0]; - } - - if ($this->GetBuffer("AdaptivePhaseKnown") !== "1") { - $referenceMaxCurrent = $this->GetAdaptiveReferenceMaxCurrent(); - if ($referenceMaxCurrent < 6.0) { - return [0]; - } - - // Bis zur sicheren Phasenerkennung wird nur ein bilanzierter - // 6-A-Prüfschritt angeboten. Is_1_ph bleibt dabei absichtlich false. - return [(int)round(6.0 * 1.71 * 400.0)]; - } - - $availableSteps = $this->Get_Array_From_Current( - $this->GetValue("Is_1_ph"), - (float)$this->GetValue("Max_Current"), - $this->GetValue("Aktuelle_Leistung"), - $this->GetValue("IsTimerActive_Null_Timer") - ); - if (count($availableSteps) === 0) { - $availableSteps = [0]; - } - - if (!$peak && !$solarladen) { - return [max($availableSteps)]; - } - - if ($solarladen && $peak) { - $minimumPower = $this->GetValue("Mindestaldestrom") * - ($this->GetValue("Is_1_ph") ? 230 : 400 * 1.71); - return [$minimumPower]; - } - - return $availableSteps; - } - // Methode zum Abrufen der aktuellen Daten public function GetCurrentData(bool $Peak) { - if ($this->GetValue("IsTimerActive")) { - $this->SetValue("Pending_Counter", 0); - if ( - $this->GetValue("Car_is_full") || - !$this->GetValue("Ladebereit") - ) { - $this->ResetTimer(); - $this->SetValue("PowerSteps", json_encode([0])); - return [0]; - } - $this->SetValue( - "PowerSteps", - json_encode([$this->GetValue("Aktuelle_Leistung")]) - ); - $this->SetValue( - "Leistung_Delta", - $this->GetValue("Aktuelle_Leistung") - - $this->GetValue("Ladeleistung_Effektiv") - ); - return [$this->GetValue("Aktuelle_Leistung")]; + // Überprüfen, ob der Timer aktiv ist + if ($this->GetValue("IsTimerActive")) { + // Timer ist aktiv, PowerSteps setzen + $this->SetValue("PowerSteps", json_encode([$this->GetValue("Aktuelle_Leistung")])); + $this->SetValue("Leistung_Delta", $this->GetValue("Aktuelle_Leistung")-$this->GetValue("Ladeleistung_Effektiv")); + + return; + }else{ + $this->SetValue("Leistung_Delta", 0); } - $this->SetValue("Leistung_Delta", 0); - $ladebereit = (bool)$this->GetValue("Ladebereit"); - $solarladen = (bool)$this->GetValue("Solarladen"); + // Aktuelle Properties abrufen + $ladebereit = GetValue($this->GetIDForIdent("Ladebereit")); + $solarladen = GetValue($this->GetIDForIdent("Solarladen")); + + + // Peak-Wert speichern $this->SetValue("Peak", $Peak); - $stationType = $this->ReadPropertyInteger("Ladestation"); - $phaseWasDetected = $this->UpdateFallbackPhaseDetection( - $stationType, - $ladebereit - ); + // Array für die Powersteps initialisieren + $powerSteps = []; - // Erst die gelernte Grenze aktualisieren, damit der Manager bereits in - // diesem Zyklus nur noch tatsächlich erreichbare Schritte erhält. - if (!$phaseWasDetected) { - $this->UpdateAdaptiveCurrentLimit( - $stationType, - $ladebereit - ); + // Konfiguration des powerSteps-Arrays basierend auf den Properties + if (!$ladebereit || $this->GetValue("Car_is_full")) { + $powerSteps = [0]; + } elseif (!$Peak && !$solarladen) { + $powerSteps = [max($this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")))]; + } elseif (!$Peak && $solarladen) { + $powerSteps = $this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")); + } elseif ($solarladen && $Peak) { + if ($this->GetValue("Is_1_ph")) { + $powerSteps = [$this->GetValue("Mindestaldestrom") * 230]; + } else { + $powerSteps = [$this->GetValue("Mindestaldestrom") * 400 * 1.71]; + } + + } else { + $powerSteps = $this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")); + } + + // PowerSteps in der RegisterVariable speichern + SetValue($this->GetIDForIdent("PowerSteps"), json_encode($powerSteps)); + // Rückgabe der Powersteps + if (!$this->UsesDirectStationDetection($this->ReadPropertyInteger("Ladestation"))) { + $counter = $this->GetValue("Pending_Counter"); + + if($this->GetValue("Aktuelle_Leistung")>(1.11*$this->GetValue("Ladeleistung_Effektiv"))){ + $this->SetValue("Pending_Counter", $counter+1); + + }elseif(max($this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")))<(1.11*$this->GetValue("Ladeleistung_Effektiv"))){ // diesen elseif ggf wieder rauslöschen + $this->SetValue("Pending_Counter", $counter+1); + + } + else{ + + $this->SetValue("Pending_Counter", 0); + + } + + if($counter>(90/($this->ReadPropertyInteger("Interval")))){ + + $this->SetValue("Pending_Counter", 0); + + if((max(json_decode($this->GetValue("PowerSteps")))) >0){ + + if($this->GetValue("Aktuelle_Leistung")>(1.11*$this->GetValue("Ladeleistung_Effektiv"))) { + + $this->Calc_Max_Current($this->GetValue("Is_1_ph")); + + + } + + } + + } + } elseif (!in_array( + $this->ReadPropertyInteger("Ladestation"), + [1, 2], + true + )) { + $this->SetValue("Pending_Counter", 0); } - - $powerSteps = $this->BuildPowerSteps( - $Peak, - $ladebereit, - $solarladen - ); - $this->SetValue("PowerSteps", json_encode($powerSteps)); + return $powerSteps; } @@ -2005,3 +1636,4 @@ class Ladestation extends IPSModule } ?> + diff --git a/Ladestation_v2/README.md b/Ladestation_v2/README.md index 827b31b..5c6302b 100644 --- a/Ladestation_v2/README.md +++ b/Ladestation_v2/README.md @@ -1,89 +1,44 @@ # Ladestation_v2 -Das Modul steuert verschiedene Ladestationstypen. +Das Modul steuert die Ladestationstypen 1 bis 6 mit der ursprünglichen, +gemeinsamen Strom-, Phasen- und Ladeende-Erkennung. -Das Konfigurationsformular zeigt die Verbindungsdaten abhängig vom gewählten -Ladestationstyp an. Allgemeine Regel- und Zeitparameter bleiben immer sichtbar. +## Gemeinsame Pending-Counter-Logik + +Die Erkennung arbeitet bei allen Stationstypen gleich: + +1. Bei einem neu verbundenen Fahrzeug wird zunächst der konfigurierte + Maximalstrom an die Station gesendet. +2. Nach 60 Sekunden wird anhand der tatsächlichen Ladeleistung zwischen + einphasigem und dreiphasigem Laden unterschieden. +3. Der erreichbare Maximalstrom wird wie ursprünglich aus der Istleistung plus + 2,5 A Reserve berechnet und durch `Max_Current_abs` begrenzt. +4. Im laufenden Betrieb vergleicht der `Pending_Counter` Soll- und Istleistung. + Bleibt die Abweichung 90 Sekunden bestehen, wird der Maximalstrom erneut + berechnet. +5. Fällt der berechnete Maximalstrom unter 6 A, wird + `Auto fertig geladen` gesetzt. + +Eine Sollvorgabe von 0 W setzt den `Pending_Counter` zurück. Die vorhandenen +Leistungsschritte, Mindestlaufzeiten und die Solar-/Peak-Umschaltung entsprechen +der Vorversion. ## Easee -Die Ladestationstypen 5 und 6 verwenden das oben als Schnittstelle ausgewählte -`Easee SignalR Gateway`. Die Schnittstellenauswahl wird bei allen -Stationstypen angezeigt. +Die Typen 5 und 6 verwenden dieselbe Logik wie die Typen 1 bis 4. Lediglich die +Kommunikation erfolgt über das oben ausgewählte `Easee SignalR Gateway`: -- Typ 5: Easee mit zusätzlicher eCarUp-Benutzerprüfung für Solarladen -- Typ 6: Easee ohne eCarUp-Benutzerprüfung +- Fahrzeugstatus und Ladeleistung werden aus den Gateway-Observations gelesen. +- Stromvorgaben werden als `SetDynamicChargerCurrent` an das Gateway gesendet. +- Es erfolgt keine interne Anmeldung an der Easee-REST-API. -Die Easee-Zugangsdaten werden ausschließlich im Gateway eingetragen. In der -Ladestationsinstanz muss die Easee-Seriennummer hinterlegt sein. - -Das Gateway liefert: - -- Observation 47: maximaler Ladestrom der Station -- Observation 104: Stromgrenze des angeschlossenen Typ-2-Kabels -- Observation 109: Fahrzeug-/Ladestatus -- Observation 110: freigegebene Ausgangsphase -- Observation 120: aktuelle Gesamtleistung in kW -- Observations 182 bis 185: gemessene Leiterströme - -Die Fahrzeugphasen werden anhand der tatsächlich gemessenen Leiterströme -erkannt; alte Phasenwerte werden beim Trennen oder Neuverbinden verworfen. -Während einer Ladepause mit 0 W bleibt die zuletzt erkannte Phasenanzahl -erhalten. Sie ändert sich erst bei einer eindeutigen Strommessung oder beim -Abstecken des Fahrzeugs. Solange noch kein eindeutiger API-Phasenwert vorliegt, -wird nur ein bilanzierter 6-A-Prüfschritt angeboten. Nach 60 Sekunden kann die -Phase ersatzweise aus Soll- und Istleistung bestimmt werden. - -Stromvorgaben von 0 bis 32 A werden ebenfalls über das Gateway gesendet. - -## Andere Ladestationstypen - -Die Typen 1 bis 4 benötigen kein Easee-Gateway. - -### Go-e - -Die Typen 1 und 2 erkennen ein verbundenes Fahrzeug direkt über den -Go-e-Fahrzeugstatus. Die tatsächlich verwendeten Phasen werden aus den drei -gemessenen Strömen des `nrg`-Arrays bestimmt. Der maximale Ladestrom wird durch -`Max_Current_abs`, `ama`, `cbl` und einen gegebenenfalls vorhandenen -16-A-Adapter begrenzt. - -### Smart-Me Pico - -Typ 3 erkennt ein verbundenes Fahrzeug über den Pico-Ladestatus. Die -Phasenerkennung verwendet `CurrentL1`, `CurrentL2` und `CurrentL3` des -Smart-Me-Geräte-Endpunkts. Der maximale Stationsstrom wird direkt aus den -Pico-Ladedaten übernommen. - -## Gemeinsames Soll-/Ist-Modell - -Der konfigurierte Maximalstrom ist bei allen Typen die absolute Obergrenze. -Statische, zuverlässige API-Grenzen dürfen sie reduzieren. Dynamische -Sollwerte der Station werden nicht als Fahrzeuggrenze verwendet. - -Bei jedem stabil zugewiesenen Leistungsschritt wird der Sollstrom mit dem -Iststrom verglichen. Bleibt der Sollstrom 90 Sekunden lang größer als der -Iststrom mal `1.11`, wird der erreichbare Maximalstrom aus `Iststrom * 1.11` -gelernt und `PowerSteps` noch im selben Zyklus neu aufgebaut. Der Lernwert -bleibt bis zum Ausstecken, einem neuen Fahrzeug oder einem Phasenwechsel aktiv. - -Fällt der berechnete Wert unter 6 A, wird `Auto fertig geladen` gesetzt und -nur `[0]` angeboten. Bei Easee gilt das nur bei verbundenem Gateway und -Fahrzeugstatus 4 oder 6: Das Modul muss dabei trotz positiver Sollvorgabe -90 Sekunden lang einen Iststrom messen, der mit Faktor `1.11` unter 6 A -bleibt. Status 4 oder 6 allein wird nicht als Ladeende gewertet. Eine -Sollvorgabe von 0 W setzt die Zähler zurück und kann weder eine -Strombegrenzung noch ein Ladeende auslösen. Das bestätigte Ladeende bleibt bis -zum Ausstecken gespeichert. +Typ 5 behält die eCarUp-Benutzerprüfung für die automatische Umschaltung von +`Solarladen`. Typ 6 verwendet keine Benutzerprüfung. ## Einrichtung 1. Eine Instanz `Easee SignalR Gateway` anlegen. 2. Easee-Benutzername und Passwort im Gateway eintragen. 3. In `Ladestation_v2` Typ 5 oder 6 sowie die Easee-Seriennummer wählen. -4. Oben über `Schnittstelle konfigurieren` beziehungsweise - `Schnittstelle ändern` die gewünschte Gateway-Instanz auswählen. +4. Das Gateway oben über `Schnittstelle konfigurieren/ändern` auswählen. -Mehrere Easee-Ladestationen desselben Kontos verwenden gemeinsam ein Gateway. -Beim Erstellen einer weiteren Instanz wird ein vorhandenes Gateway automatisch -verwendet; es wird keine zweite Parent-Verbindung erzeugt. diff --git a/Ladestation_v2/form.json b/Ladestation_v2/form.json index 4722992..e7762c9 100644 --- a/Ladestation_v2/form.json +++ b/Ladestation_v2/form.json @@ -113,3 +113,5 @@ } ] } + + diff --git a/Ladestation_v2/module.json b/Ladestation_v2/module.json index 0dba1f7..7d5ab8c 100644 --- a/Ladestation_v2/module.json +++ b/Ladestation_v2/module.json @@ -14,3 +14,5 @@ "prefix": "GEF", "url": "" } + + diff --git a/Ladestation_v2/module.php b/Ladestation_v2/module.php index 41137d2..d0f985e 100644 --- a/Ladestation_v2/module.php +++ b/Ladestation_v2/module.php @@ -5,10 +5,6 @@ class Ladestation_v2 extends IPSModule private const EASEE_GATEWAY_MODULE_ID = "{7A1F1D4B-3E7D-4A3B-9E54-61C8C3B7F201}"; private const EASEE_GATEWAY_REQUEST_ID = "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}"; private const EASEE_GATEWAY_EVENT_ID = "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}"; - private const ADAPTIVE_MODEL_VERSION = "5"; - private const ADAPTIVE_MISMATCH_FACTOR = 1.11; - private const ADAPTIVE_CONFIRMATION_SECONDS = 90; - private const PHASE_CONFIRMATION_SECONDS = 60; public function Create() { @@ -34,7 +30,6 @@ class Ladestation_v2 extends IPSModule // Ladestationspezifische Variabeln $this->RegisterVariableFloat("Max_Current","Maximaler Ladestrom","", 0); IPS_SetHidden($this->GetIDForIdent("Max_Current"), true); - $this->SetValue("Max_Current", (float)$this->ReadPropertyInteger("Max_Current_abs")); $this->RegisterVariableBoolean("Is_1_ph", "Auto lädt 1-Phasig", "~Switch", false); $this->RegisterVariableBoolean("Car_detected", "Auto erkannt","~Switch", false); $this->RegisterVariableInteger("Pending_Counter", "Pending_Counter", "", 0); @@ -104,19 +99,6 @@ class Ladestation_v2 extends IPSModule ); IPS_SetHidden($this->GetIDForIdent("Easee_Gateway_Connected"), true); $this->SetBuffer("EaseeGatewayState", "{}"); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetBuffer("AdaptiveMeasurementTimestamp", "0"); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $this->SetBuffer( - "AdaptiveStationType", - (string)$this->ReadPropertyInteger("Ladestation") - ); - $this->SetBuffer("AdaptiveModelVersion", self::ADAPTIVE_MODEL_VERSION); // Hilfsvariabeln für Idle zustand $this->RegisterPropertyInteger("IdleCounterMax", 2); @@ -299,46 +281,6 @@ class Ladestation_v2 extends IPSModule ); $this->SetTimerInterval("Timer_Do_UserCalc_EVC",$this->ReadPropertyInteger("Interval")*1000); $stationType = $this->ReadPropertyInteger("Ladestation"); - - // Einmalige Migration aus den bisherigen stationsspezifischen - // Lernalgorithmen in das gemeinsame Soll-/Ist-Modell. - if ( - $this->GetBuffer("AdaptiveModelVersion") !== self::ADAPTIVE_MODEL_VERSION || - (int)$this->GetBuffer("AdaptiveStationType") !== $stationType - ) { - $this->SetValue("Car_detected", false); - $this->SetValue("Car_is_full", false); - $this->SetValue("Is_1_ph", false); - $this->SetValue("Pending_Counter", 0); - $this->SetValue( - "Max_Current", - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$this->ReadPropertyInteger("Max_Current_abs") - ); - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetBuffer("AdaptiveMeasurementTimestamp", "0"); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $this->SetBuffer("AdaptiveStationType", (string)$stationType); - $this->SetBuffer("SmartMeChargingEnabled", "0"); - $this->SetBuffer("SmartMeMinStationCurrent", "6"); - $this->SetBuffer("AdaptiveModelVersion", self::ADAPTIVE_MODEL_VERSION); - } - - if (!$this->GetValue("Car_detected")) { - $configuredMaxCurrent = (float)$this->ReadPropertyInteger( - "Max_Current_abs" - ); - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$configuredMaxCurrent - ); - $this->SetValue("Max_Current", $configuredMaxCurrent); - } - $usesEaseeGateway = $this->UsesEaseeGateway($stationType); IPS_SetHidden( $this->GetIDForIdent("Easee_Gateway_Connected"), @@ -546,280 +488,11 @@ class Ladestation_v2 extends IPSModule return $stationType === 5 || $stationType === 6; } - private function UsesAdaptiveCurrentDetection(int $stationType): bool + private function UsesDirectStationDetection(int $stationType): bool { - return in_array($stationType, [1, 2, 3, 4, 5, 6], true); - } - - private function CanUseAdaptiveFullDetection(int $stationType): bool - { - if (!$this->UsesEaseeGateway($stationType)) { - return true; - } - - // Easee-Status 4 bedeutet "pausiert/gestoppt", Status 6 bedeutet, - // dass die Station auf eine Stromaufnahme des Fahrzeugs wartet. Beide - // sind nur zusammen mit positivem Soll und einer 90 Sekunden lang - // bestätigten, zu geringen Stromaufnahme ein Ladeende-Kandidat. - return $this->GetValue("Easee_Gateway_Connected") && - in_array((int)$this->GetValue("Fahrzeugstatus"), [4, 6], true); - } - - private function SetAdaptiveReferenceMaxCurrent(float $apiMaxCurrent): float - { - $configuredMaxCurrent = max( - 0.0, - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - $referenceMaxCurrent = $configuredMaxCurrent; - - // Nur plausible statische Stations-/Kabelgrenzen aus einer API dürfen - // die konfigurierte Obergrenze reduzieren. - if ($apiMaxCurrent >= 6.0) { - $referenceMaxCurrent = min($configuredMaxCurrent, $apiMaxCurrent); - } - - $this->SetBuffer( - "AdaptiveReferenceMaxCurrent", - (string)$referenceMaxCurrent - ); - return $referenceMaxCurrent; - } - - private function GetAdaptiveReferenceMaxCurrent(): float - { - $referenceMaxCurrent = (float)$this->GetBuffer( - "AdaptiveReferenceMaxCurrent" - ); - if ($referenceMaxCurrent <= 0) { - $referenceMaxCurrent = max( - 0.0, - (float)$this->ReadPropertyInteger("Max_Current_abs") - ); - } - - return min( - $referenceMaxCurrent, - max(0.0, (float)$this->ReadPropertyInteger("Max_Current_abs")) - ); - } - - private function ResetAdaptiveLearning(float $referenceMaxCurrent) - { - $this->SetBuffer("AdaptiveLearnedMaxCurrent", "0"); - $this->SetValue("Pending_Counter", 0); - $this->SetValue("Max_Current", $referenceMaxCurrent); - } - - private function StoreAdaptiveMeasurement(int $timestamp = 0) - { - $this->SetBuffer( - "AdaptiveMeasurementTimestamp", - (string)($timestamp > 0 ? $timestamp : time()) - ); - } - - private function HasFreshAdaptiveMeasurement(int $stationType): bool - { - // Der Dummy besitzt keine API-Zeitmarke und verwendet weiterhin den - // direkt gesetzten Istwert. - if ($stationType === 4) { - return true; - } - - $timestamp = (int)$this->GetBuffer("AdaptiveMeasurementTimestamp"); - $maximumAge = max( - 30, - 3 * max(1, $this->ReadPropertyInteger("Interval")) - ); - return $timestamp > 0 && (time() - $timestamp) <= $maximumAge; - } - - private function IsAdaptiveChargingEnabled(int $stationType): bool - { - if ($stationType === 5) { - return $this->ReadPropertyString("Username") === - $this->GetValue("Letzer_User"); - } - - // Die verbindliche Freigabe ist die positive Sollleistung des Managers. - // Statusfelder der Station können beim echten Ladeende bereits 0 sein - // und dürfen deshalb die Ladeende-Erkennung nicht blockieren. - return true; - } - - private function UpdateFallbackPhaseDetection( - int $stationType, - bool $ladebereit - ): bool { - if ( - $stationType === 4 || - $this->GetBuffer("AdaptivePhaseKnown") === "1" - ) { - $this->SetBuffer("AdaptivePhaseCounter", "0"); - return false; - } - - $requestedPower = max( - 0.0, - (float)$this->GetValue("Aktuelle_Leistung") - ); - $requestedCurrent = $requestedPower / (1.71 * 400.0); - $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && - $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $this->GetValue("Idle") && - $requestedCurrent >= 5.5 && - $requestedCurrent <= 6.5 && - $this->IsAdaptiveChargingEnabled($stationType) && - $this->HasFreshAdaptiveMeasurement($stationType); - - if (!$canEvaluate) { - $this->SetBuffer("AdaptivePhaseCounter", "0"); - return false; - } - - $counter = (int)$this->GetBuffer("AdaptivePhaseCounter") + 1; - $this->SetBuffer("AdaptivePhaseCounter", (string)$counter); - $requiredCycles = max( - 1, - (int)ceil( - self::PHASE_CONFIRMATION_SECONDS / - max(1, $this->ReadPropertyInteger("Interval")) - ) - ); - if ($counter < $requiredCycles) { - return false; - } - - $this->SetBuffer("AdaptivePhaseCounter", "0"); - $actualPower = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ); - // Ohne messbaren Strom ist keine Phasenerkennung möglich. Vor allem - // darf eine Ladepause die zuletzt bekannte Phase nicht auf 1-phasig - // umschalten. Die Ladeende-Erkennung läuft separat weiter. - if ($actualPower < 100.0) { - return false; - } - - $onePhaseExpectedPower = $requestedCurrent * 230.0; - $threePhaseExpectedPower = $requestedCurrent * 1.71 * 400.0; - $isOnePhase = $actualPower < - (($onePhaseExpectedPower + $threePhaseExpectedPower) / 2.0); - - $this->SetValue("Is_1_ph", $isOnePhase); - $this->SetBuffer("AdaptivePhaseKnown", "1"); - $this->ResetAdaptiveLearning($this->GetAdaptiveReferenceMaxCurrent()); - - $phaseDivisor = $isOnePhase ? 230.0 : 1.71 * 400.0; - $this->SetValue( - "Power", - (int)round($this->GetValue("Mindestaldestrom") * $phaseDivisor) - ); - $detectedCurrent = self::ADAPTIVE_MISMATCH_FACTOR * - ($actualPower / $phaseDivisor); - if ( - $detectedCurrent < 6.0 && - $this->CanUseAdaptiveFullDetection($stationType) - ) { - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$detectedCurrent - ); - $this->SetValue("Max_Current", $detectedCurrent); - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - } - - return true; - } - - private function UpdateAdaptiveCurrentLimit( - int $stationType, - bool $ladebereit - ): bool { - $requestedPower = max( - 0.0, - (float)$this->GetValue("Aktuelle_Leistung") - ); - $actualPower = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ); - $hasKnownPhase = $stationType === 4 || - $this->GetBuffer("AdaptivePhaseKnown") === "1"; - - $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && - $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $this->GetValue("Idle") && - ($hasKnownPhase || $actualPower < 100.0) && - $requestedPower > 0 && - $this->IsAdaptiveChargingEnabled($stationType) && - $this->HasFreshAdaptiveMeasurement($stationType); - - if (!$canEvaluate) { - $this->SetValue("Pending_Counter", 0); - return false; - } - - $phaseDivisor = $this->GetValue("Is_1_ph") ? 230.0 : 1.71 * 400.0; - $requestedCurrent = $requestedPower / $phaseDivisor; - $actualCurrent = $actualPower / $phaseDivisor; - - if ( - $requestedCurrent <= - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ) { - $this->SetValue("Pending_Counter", 0); - return false; - } - - $counter = $this->GetValue("Pending_Counter") + 1; - $this->SetValue("Pending_Counter", $counter); - $requiredCycles = max( - 1, - (int)ceil( - self::ADAPTIVE_CONFIRMATION_SECONDS / - max(1, $this->ReadPropertyInteger("Interval")) - ) - ); - - if ($counter < $requiredCycles) { - return false; - } - - $this->SetValue("Pending_Counter", 0); - $learnedMaxCurrent = min( - $this->GetAdaptiveReferenceMaxCurrent(), - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ); - - if ($learnedMaxCurrent < 6.0) { - if (!$this->CanUseAdaptiveFullDetection($stationType)) { - return false; - } - - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue("Max_Current", $learnedMaxCurrent); - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - return true; - } - - $this->SetBuffer( - "AdaptiveLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue("Max_Current", $learnedMaxCurrent); - return true; + // Alle Stationstypen verwenden wieder die gemeinsame, ursprüngliche + // Strom-/Phasenerkennung mit Pending_Counter. + return false; } private function ApplyEaseeGatewayResponse($response) @@ -841,7 +514,7 @@ class Ladestation_v2 extends IPSModule } $updated = (int)($response["state"]["updated"] ?? time()); - foreach ([47, 104, 109, 110, 120, 182, 183, 184, 185] as $observationID) { + foreach ([47, 109, 110, 120, 182, 183, 184, 185] as $observationID) { $key = (string)$observationID; if (array_key_exists($key, $response["state"])) { $this->StoreEaseeObservation( @@ -855,7 +528,7 @@ class Ladestation_v2 extends IPSModule private function StoreEaseeObservation(int $observationID, $value, int $timestamp) { - if (!in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true)) { + if (!in_array($observationID, [47, 109, 110, 120, 182, 183, 184, 185], true)) { return; } @@ -881,7 +554,6 @@ class Ladestation_v2 extends IPSModule $this->SetValue("Fahrzeugstatus", (int)$value); } elseif ($observationID === 120) { $this->SetValue("Ladeleistung_Effektiv", round((float)$value * 1000)); - $this->StoreAdaptiveMeasurement($timestamp); } } @@ -909,13 +581,18 @@ class Ladestation_v2 extends IPSModule private function DetectEaseeCar(int $carType): bool { + $wasDetected = $this->GetValue("Car_detected"); + $wasOnePhase = $this->GetValue("Is_1_ph"); $carDetected = $this->Get_Car_Status($carType); $state = json_decode($this->GetBuffer("EaseeGatewayState"), true); if (!is_array($state)) { $state = []; } - $isOnePhase = $this->DetectEaseePhases($state); + $this->SetValue("Pending_Counter", 0); + + $this->DetectEaseePhases($state); + $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); // Observation 47 ist der nichtflüchtige maximale Ladestrom der // Station. Die lokal konfigurierte Obergrenze bleibt zusätzlich aktiv. @@ -923,26 +600,39 @@ class Ladestation_v2 extends IPSModule if (array_key_exists("47", $state) && (float)$state["47"] > 0) { $maxCurrent = min($maxCurrent, (float)$state["47"]); } - // Observation 104 ist die vom Typ-2-Kabel signalisierte Stromgrenze. - if (array_key_exists("104", $state) && (float)$state["104"] >= 6) { - $maxCurrent = min($maxCurrent, (float)$state["104"]); - } + $this->SetValue("Max_Current", $maxCurrent); - $chargerOpMode = (int)($state["109"] ?? -1); - if (!$carDetected && $chargerOpMode !== 1) { - // Einen vorübergehenden Fehlerstatus nicht als Ausstecken werten. + if (!$carDetected) { $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); - $this->SetValue("Pending_Counter", 0); + + $chargerOpMode = (int)($state["109"] ?? -1); + if ($chargerOpMode === 1) { + $this->SetValue("Leistung_Delta", 0); + $this->ResetTimer(); + $this->ResetNullTimer(); + $this->SetValue("Is_1_ph", false); + $this->SetValue("Aktuelle_Leistung", 0); + $this->SetValue("IdleCounter", 0); + $this->SetValue("Idle", true); + } + return false; } - return $this->ApplyDirectStationDetection( - $carDetected, - false, - $isOnePhase, - $maxCurrent - ); + $this->SetValue("Car_detected", true); + $this->SetValue("Car_is_full", (int)($state["109"] ?? 0) === 4); + + if (!$wasDetected || $phaseChanged) { + if ($this->GetValue("Is_1_ph")) { + $power = $this->GetValue("Mindestaldestrom") * 230; + } else { + $power = $this->GetValue("Mindestaldestrom") * 400 * 1.71; + } + $this->SetValue("Power", $power); + } + + return true; } private function DetectEaseePhases(array $state) @@ -955,7 +645,7 @@ class Ladestation_v2 extends IPSModule array_key_exists("120", $state) && abs((float)$state["120"]) < 0.1 ) { - return null; + return; } $activePhaseConductors = 0; @@ -966,25 +656,24 @@ class Ladestation_v2 extends IPSModule } if ($activePhaseConductors === 0) { - return null; + return; } // Ein explizit einphasiger Easee-Ausgang ist auch in IT-Netzen // einphasig, obwohl dort Strom auf zwei Leitern gemessen wird. if ($outputPhase >= 10 && $outputPhase <= 15) { - return true; + $this->SetValue("Is_1_ph", true); + return; } // Bei einem dreiphasig freigegebenen Ausgang kann ein einphasiges Auto // trotzdem nur auf einem Leiter Strom beziehen. Observation 110 allein // kann diesen Fahrzeugwechsel deshalb nicht erkennen. if ($activePhaseConductors === 1) { - return true; + $this->SetValue("Is_1_ph", true); } elseif ($activePhaseConductors >= 2) { - return false; + $this->SetValue("Is_1_ph", false); } - - return null; } private function ReadJsonEndpoint(string $url, bool $useAuthentication = false) @@ -1044,19 +733,19 @@ class Ladestation_v2 extends IPSModule bool $carDetected, bool $carIsFull, $isOnePhase, - float $maxCurrent + float $maxCurrent, + bool $resetPendingCounter = true ): bool { $wasDetected = $this->GetValue("Car_detected"); $wasOnePhase = $this->GetValue("Is_1_ph"); - $wasFull = $this->GetValue("Car_is_full"); - $referenceMaxCurrent = $this->SetAdaptiveReferenceMaxCurrent( - $maxCurrent - ); + + if ($resetPendingCounter) { + $this->SetValue("Pending_Counter", 0); + } + $this->SetValue("Max_Current", $maxCurrent); if (!$carDetected) { - $this->ResetAdaptiveLearning($referenceMaxCurrent); - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); + $this->SetValue("Pending_Counter", 0); $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); $this->SetValue("Leistung_Delta", 0); @@ -1071,36 +760,12 @@ class Ladestation_v2 extends IPSModule if (is_bool($isOnePhase)) { $this->SetValue("Is_1_ph", $isOnePhase); - $this->SetBuffer("AdaptivePhaseKnown", "1"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - } elseif (!$wasDetected) { - $this->SetBuffer("AdaptivePhaseKnown", "0"); - $this->SetBuffer("AdaptivePhaseCounter", "0"); - } - - $phaseChanged = $wasDetected && - is_bool($isOnePhase) && - $wasOnePhase !== $this->GetValue("Is_1_ph"); - if (!$wasDetected || $phaseChanged) { - $this->ResetAdaptiveLearning($referenceMaxCurrent); - } else { - $learnedMaxCurrent = (float)$this->GetBuffer( - "AdaptiveLearnedMaxCurrent" - ); - $this->SetValue( - "Max_Current", - $learnedMaxCurrent > 0 - ? min($referenceMaxCurrent, $learnedMaxCurrent) - : $referenceMaxCurrent - ); } $this->SetValue("Car_detected", true); - $this->SetValue( - "Car_is_full", - $carIsFull || ($wasDetected && !$phaseChanged && $wasFull) - ); + $this->SetValue("Car_is_full", $carIsFull); + $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); if (!$wasDetected || $phaseChanged) { if ($this->GetValue("Is_1_ph")) { $power = $this->GetValue("Mindestaldestrom") * 230; @@ -1126,10 +791,6 @@ class Ladestation_v2 extends IPSModule } } - if (!empty($data["adi"])) { - $maxCurrent = min($maxCurrent, 16.0); - } - if (isset($data["var"]) && is_numeric($data["var"])) { $variant = (int)$data["var"]; if ($variant === 11) { @@ -1148,14 +809,8 @@ class Ladestation_v2 extends IPSModule return (int)$data["alw"] === 1; } - if (array_key_exists("frc", $data)) { - $forceState = (int)$data["frc"]; - if ($forceState === 1) { - return false; - } - if ($forceState === 2) { - return true; - } + if ($stationType === 2 && array_key_exists("frc", $data)) { + return (int)$data["frc"] === 2; } if (array_key_exists("alw", $data)) { @@ -1165,6 +820,53 @@ class Ladestation_v2 extends IPSModule return $this->GetBuffer("GoEChargingEnabled") === "1"; } + private function DetectGoEFullCharge( + array $data, + int $stationType, + int $carState, + $chargingPowerWatts + ): bool { + $wasFull = $this->GetValue("Car_is_full"); + + // Ein einmal sicher erkanntes Ladeende bleibt bis zum Abstecken oder + // bis zu einer tatsächlich wieder aufgenommenen Ladung erhalten. + if ($wasFull) { + if ( + is_numeric($chargingPowerWatts) && + abs((float)$chargingPowerWatts) >= 100 + ) { + $this->SetValue("Pending_Counter", 0); + return false; + } + + return true; + } + + // car=4 alleine ist nicht ausreichend: Go-E meldet diesen Status auch, + // wenn die Regelung die Ladung absichtlich unterbricht. + if ( + $carState !== 4 || + !$this->IsGoEChargingEnabled($data, $stationType) || + !is_numeric($chargingPowerWatts) || + abs((float)$chargingPowerWatts) >= 100 + ) { + $this->SetValue("Pending_Counter", 0); + return false; + } + + $counter = $this->GetValue("Pending_Counter") + 1; + $this->SetValue("Pending_Counter", $counter); + + $interval = max(1, $this->ReadPropertyInteger("Interval")); + $requiredCycles = max(1, (int)ceil(90 / $interval)); + if ($counter < $requiredCycles) { + return false; + } + + $this->SetValue("Pending_Counter", 0); + return true; + } + private function DetectGoECar(int $stationType): bool { $path = $stationType === 1 ? "/mqtt?payload=" : "/api/status"; @@ -1182,10 +884,6 @@ class Ladestation_v2 extends IPSModule } $this->SetValue("Fahrzeugstatus", $carState); - $this->SetBuffer( - "GoEChargingEnabledFromStatus", - $this->IsGoEChargingEnabled($data, $stationType) ? "1" : "0" - ); $isOnePhase = null; $chargingPowerWatts = null; @@ -1207,27 +905,28 @@ class Ladestation_v2 extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); - $this->StoreAdaptiveMeasurement(); } } $carDetected = in_array($carState, [2, 3, 4, 5], true); - $stationMaxCurrent = $this->GetGoEMaxCurrent($data); + $carIsFull = $carDetected && $this->DetectGoEFullCharge( + $data, + $stationType, + $carState, + $chargingPowerWatts + ); return $this->ApplyDirectStationDetection( $carDetected, - false, + $carIsFull, $isOnePhase, - $stationMaxCurrent + $this->GetGoEMaxCurrent($data), + false ); } private function DetectSmartMeCar(): bool { - // Bei einem fehlgeschlagenen Abruf darf eine alte Freigabe keinesfalls - // zur Ladeende-Erkennung weiterverwendet werden. - $this->SetBuffer("SmartMeChargingEnabled", "0"); - $deviceID = trim($this->ReadPropertyString("ID")); if ($deviceID === "") { return $this->GetValue("Car_detected"); @@ -1255,51 +954,13 @@ class Ladestation_v2 extends IPSModule $chargingPower = $chargingData["ActiveChargingPower"] ?? $chargingData["activeChargingPower"] ?? null; - $chargingPowerWatts = null; if (is_numeric($chargingPower)) { - $chargingPowerWatts = (float)$chargingPower * 1000; $this->SetValue( "Ladeleistung_Effektiv", - round($chargingPowerWatts) + round((float)$chargingPower * 1000) ); - $this->StoreAdaptiveMeasurement(); } - // Die Pico-API unterscheidet zwischen der extern vorgegebenen - // Stromgrenze und der effektiv verfügbaren Stromgrenze. Nur wenn beide - // vorhandenen Grenzen mindestens dem Stationsminimum entsprechen, ist - // die Ladung wirklich freigegeben. Eine Regelungsunterbrechung mit 0 A - // darf nicht als abgeschlossenes Laden ausgewertet werden. - $minStationCurrent = $chargingData["MinStationCurrent"] ?? - $chargingData["minStationCurrent"] ?? 6; - $minStationCurrent = is_numeric($minStationCurrent) - ? max(1.0, (float)$minStationCurrent) - : 6.0; - $this->SetBuffer( - "SmartMeMinStationCurrent", - (string)$minStationCurrent - ); - - $maxDynamicCurrent = $chargingData["MaxDynamicCurrent"] ?? - $chargingData["maxDynamicCurrent"] ?? null; - $maxAllowedCurrent = $chargingData["MaxAllowedChargingCurrent"] ?? - $chargingData["maxAllowedChargingCurrent"] ?? null; - - $hasCurrentLimit = is_numeric($maxDynamicCurrent) || - is_numeric($maxAllowedCurrent); - $dynamicCurrentAllowsCharging = !is_numeric($maxDynamicCurrent) || - (float)$maxDynamicCurrent >= $minStationCurrent; - $allowedCurrentAllowsCharging = !is_numeric($maxAllowedCurrent) || - (float)$maxAllowedCurrent >= $minStationCurrent; - $chargingEnabled = $hasCurrentLimit - ? $dynamicCurrentAllowsCharging && $allowedCurrentAllowsCharging - : $this->GetValue("Aktuelle_Leistung") > 0; - - $this->SetBuffer( - "SmartMeChargingEnabled", - $chargingEnabled ? "1" : "0" - ); - $isOnePhase = null; $deviceData = $this->ReadJsonEndpoint( "https://api.smart-me.com/Devices/" . $deviceID, @@ -1318,6 +979,8 @@ class Ladestation_v2 extends IPSModule foreach ([ "MaxStationCurrent", "maxStationCurrent", + "MaxLoadmanagementGroupCurrent", + "maxLoadmanagementGroupCurrent", ] as $key) { if (isset($chargingData[$key]) && is_numeric($chargingData[$key])) { $candidate = (float)$chargingData[$key]; @@ -1327,10 +990,8 @@ class Ladestation_v2 extends IPSModule } } - $carDetected = in_array($stationState, [2, 3, 4, 6], true); - return $this->ApplyDirectStationDetection( - $carDetected, + in_array($stationState, [2, 3, 4, 6], true), false, $isOnePhase, $maxCurrent @@ -1358,19 +1019,6 @@ class Ladestation_v2 extends IPSModule public function Detect_Car(int $carType) { - - if ($carType === 1 || $carType === 2) { - return $this->DetectGoECar($carType); - } - - if ($carType === 3) { - return $this->DetectSmartMeCar(); - } - - if ($this->UsesEaseeGateway($carType)) { - return $this->DetectEaseeCar($carType); - } - if($this->GetValue("Car_detected")==true){ if($this->Get_Car_Status($carType)==true){ $this->SetValue("Car_detected", true); @@ -1406,13 +1054,11 @@ class Ladestation_v2 extends IPSModule $this->SetValue("Pending_Counter", 0); if($this->GetValue("Ladeleistung_Effektiv")>7500){ $this->SetValue("Is_1_ph", false); - $this->SetBuffer("AdaptivePhaseKnown", "1"); $this->Calc_Max_Current(false); } else{ $this->SetValue("Is_1_ph", true); - $this->SetBuffer("AdaptivePhaseKnown", "1"); $this->Calc_Max_Current(true); } @@ -1642,26 +1288,19 @@ class Ladestation_v2 extends IPSModule public function Calc_Max_Current(bool $is_1_ph) { - $phaseDivisor = $is_1_ph ? 230.0 : 1.71 * 400.0; - $actualCurrent = abs( - (float)$this->GetValue("Ladeleistung_Effektiv") - ) / $phaseDivisor; - $maxCurrent = min( - $this->GetAdaptiveReferenceMaxCurrent(), - self::ADAPTIVE_MISMATCH_FACTOR * $actualCurrent - ); + $maxCurrent = 32; + if($is_1_ph){ - $this->SetBuffer("AdaptiveLearnedMaxCurrent", (string)$maxCurrent); - $this->SetValue("Max_Current", $maxCurrent); - - if ( - $maxCurrent < 6.0 && - $this->CanUseAdaptiveFullDetection( - $this->ReadPropertyInteger("Ladestation") - ) - ) { - $this->SetValue("Car_is_full", true); + $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / 230); } + else{ + + $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / (1.71*400)); + } + if($maxCurrent>$this->ReadPropertyInteger("Max_Current_abs")){ + $maxCurrent = $this->ReadPropertyInteger("Max_Current_abs"); + } + $this->SetValue("Max_Current", $maxCurrent); } public function Get_Current_From_Power(bool $is_1_ph, int $power) @@ -1791,101 +1430,93 @@ class Ladestation_v2 extends IPSModule } } - private function BuildPowerSteps( - bool $peak, - bool $ladebereit, - bool $solarladen - ): array { - if (!$ladebereit || $this->GetValue("Car_is_full")) { - return [0]; - } - - if ($this->GetBuffer("AdaptivePhaseKnown") !== "1") { - $referenceMaxCurrent = $this->GetAdaptiveReferenceMaxCurrent(); - if ($referenceMaxCurrent < 6.0) { - return [0]; - } - - // Bis zur sicheren Phasenerkennung wird nur ein bilanzierter - // 6-A-Prüfschritt angeboten. Is_1_ph bleibt dabei absichtlich false. - return [(int)round(6.0 * 1.71 * 400.0)]; - } - - $availableSteps = $this->Get_Array_From_Current( - $this->GetValue("Is_1_ph"), - (float)$this->GetValue("Max_Current"), - $this->GetValue("Aktuelle_Leistung"), - $this->GetValue("IsTimerActive_Null_Timer") - ); - if (count($availableSteps) === 0) { - $availableSteps = [0]; - } - - if (!$peak && !$solarladen) { - return [max($availableSteps)]; - } - - if ($solarladen && $peak) { - $minimumPower = $this->GetValue("Mindestaldestrom") * - ($this->GetValue("Is_1_ph") ? 230 : 400 * 1.71); - return [$minimumPower]; - } - - return $availableSteps; - } - // Methode zum Abrufen der aktuellen Daten public function GetCurrentData(bool $Peak) { - if ($this->GetValue("IsTimerActive")) { - $this->SetValue("Pending_Counter", 0); - if ( - $this->GetValue("Car_is_full") || - !$this->GetValue("Ladebereit") - ) { - $this->ResetTimer(); - $this->SetValue("PowerSteps", json_encode([0])); - return [0]; - } - $this->SetValue( - "PowerSteps", - json_encode([$this->GetValue("Aktuelle_Leistung")]) - ); - $this->SetValue( - "Leistung_Delta", - $this->GetValue("Aktuelle_Leistung") - - $this->GetValue("Ladeleistung_Effektiv") - ); - return [$this->GetValue("Aktuelle_Leistung")]; + // Überprüfen, ob der Timer aktiv ist + if ($this->GetValue("IsTimerActive")) { + // Timer ist aktiv, PowerSteps setzen + $this->SetValue("PowerSteps", json_encode([$this->GetValue("Aktuelle_Leistung")])); + $this->SetValue("Leistung_Delta", $this->GetValue("Aktuelle_Leistung")-$this->GetValue("Ladeleistung_Effektiv")); + + return; + }else{ + $this->SetValue("Leistung_Delta", 0); } - $this->SetValue("Leistung_Delta", 0); - $ladebereit = (bool)$this->GetValue("Ladebereit"); - $solarladen = (bool)$this->GetValue("Solarladen"); + // Aktuelle Properties abrufen + $ladebereit = GetValue($this->GetIDForIdent("Ladebereit")); + $solarladen = GetValue($this->GetIDForIdent("Solarladen")); + + + // Peak-Wert speichern $this->SetValue("Peak", $Peak); - $stationType = $this->ReadPropertyInteger("Ladestation"); - $phaseWasDetected = $this->UpdateFallbackPhaseDetection( - $stationType, - $ladebereit - ); + // Array für die Powersteps initialisieren + $powerSteps = []; - // Erst die gelernte Grenze aktualisieren, damit der Manager bereits in - // diesem Zyklus nur noch tatsächlich erreichbare Schritte erhält. - if (!$phaseWasDetected) { - $this->UpdateAdaptiveCurrentLimit( - $stationType, - $ladebereit - ); + // Konfiguration des powerSteps-Arrays basierend auf den Properties + if (!$ladebereit || $this->GetValue("Car_is_full")) { + $powerSteps = [0]; + } elseif (!$Peak && !$solarladen) { + $powerSteps = [max($this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")))]; + } elseif (!$Peak && $solarladen) { + $powerSteps = $this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")); + } elseif ($solarladen && $Peak) { + if ($this->GetValue("Is_1_ph")) { + $powerSteps = [$this->GetValue("Mindestaldestrom") * 230]; + } else { + $powerSteps = [$this->GetValue("Mindestaldestrom") * 400 * 1.71]; + } + + } else { + $powerSteps = $this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")); + } + + // PowerSteps in der RegisterVariable speichern + SetValue($this->GetIDForIdent("PowerSteps"), json_encode($powerSteps)); + // Rückgabe der Powersteps + if (!$this->UsesDirectStationDetection($this->ReadPropertyInteger("Ladestation"))) { + $counter = $this->GetValue("Pending_Counter"); + + if($this->GetValue("Aktuelle_Leistung")>(1.11*$this->GetValue("Ladeleistung_Effektiv"))){ + $this->SetValue("Pending_Counter", $counter+1); + + }elseif(max($this->Get_Array_From_Current($this->GetValue("Is_1_ph"),$this->GetValue("Max_Current"), $this->GetValue("Aktuelle_Leistung"), $this->GetValue("IsTimerActive_Null_Timer")))<(1.11*$this->GetValue("Ladeleistung_Effektiv"))){ // diesen elseif ggf wieder rauslöschen + $this->SetValue("Pending_Counter", $counter+1); + + } + else{ + + $this->SetValue("Pending_Counter", 0); + + } + + if($counter>(90/($this->ReadPropertyInteger("Interval")))){ + + $this->SetValue("Pending_Counter", 0); + + if((max(json_decode($this->GetValue("PowerSteps")))) >0){ + + if($this->GetValue("Aktuelle_Leistung")>(1.11*$this->GetValue("Ladeleistung_Effektiv"))) { + + $this->Calc_Max_Current($this->GetValue("Is_1_ph")); + + + } + + } + + } + } elseif (!in_array( + $this->ReadPropertyInteger("Ladestation"), + [1, 2], + true + )) { + $this->SetValue("Pending_Counter", 0); } - - $powerSteps = $this->BuildPowerSteps( - $Peak, - $ladebereit, - $solarladen - ); - $this->SetValue("PowerSteps", json_encode($powerSteps)); + return $powerSteps; }