diff --git a/EaseeGateway/README.md b/EaseeGateway/README.md index 358d675..2964391 100644 --- a/EaseeGateway/README.md +++ b/EaseeGateway/README.md @@ -1,8 +1,9 @@ # Easee SignalR Gateway Das Gateway hält eine gemeinsame SignalR-Verbindung zum Easee-Konto und verteilt -die Observations 47 (Max Charger Current), 109 (Charger Op Mode), 110 -(Output Phase), 120 (Total Power) und 182 bis 185 (gemessene Leiterströme) an alle verbundenen +die Observations 47 (Max Charger Current), 104 (Cable Rating), 109 +(Charger Op Mode), 110 (Output Phase), 120 (Total Power) und 182 bis 185 +(gemessene Leiterströme) an alle verbundenen Ladestation- und Ladestation_v2-Instanzen. Benutzername und Passwort werden ausschließlich im Gateway eingetragen. Mehrere diff --git a/EaseeGateway/module.php b/EaseeGateway/module.php index 95887ca..e4f1379 100644 --- a/EaseeGateway/module.php +++ b/EaseeGateway/module.php @@ -497,7 +497,7 @@ class EaseeGateway extends IPSModule if ( $serialNumber !== "" && $valueExists && - in_array($observationID, [47, 109, 110, 120, 182, 183, 184, 185], true) + in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true) ) { $this->PublishObservation($serialNumber, $observationID, $value); } diff --git a/Ladestation/README.md b/Ladestation/README.md index ea2200f..2043e84 100644 --- a/Ladestation/README.md +++ b/Ladestation/README.md @@ -20,20 +20,19 @@ 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 -Bei Easee erfolgt die Fahrzeug- und Phasenerkennung ohne den zeitbasierten -`Pending_Counter`. Der maximale Ladestrom wird aus Observation 47 übernommen -und zusätzlich durch `Max_Current_abs` begrenzt. Die Typen 1 bis 4 verwenden -weiterhin die adaptive Soll-/Istleistungserkennung mit dem `Pending_Counter`. 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. +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. @@ -46,29 +45,32 @@ Die Typen 1 bis 4 benötigen kein Easee-Gateway. 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 -die Stations-, Kabel- und Modulgrenze begrenzt. - -Ist die angeforderte Ladeleistung während 90 Sekunden um den Faktor `1.11` -höher als die tatsächliche Leistung, wird der fahrzeugseitige Maximalstrom -aus dem Istwert neu berechnet. Fällt er unter 6 A, wird `Auto fertig geladen` -gesetzt. Bei einer durch das Modul angeforderten 0-W-Pause wird der -`Pending_Counter` zurückgesetzt. Zusätzlich muss bei Go-E die Ladung -freigegeben sein und der Fahrzeugstatus 4 anliegen. +`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. Der fahrzeugseitige Maximalstrom und das Ladeende -werden weiterhin über den 90-Sekunden-Soll-/Istvergleich ermittelt. +Pico-Ladedaten übernommen. -Bei Typ 1 bis 3 entfällt nur die frühere zeitbasierte Phasenerkennung. Die -Phasenzahl wird direkt aus den API-Messwerten aktualisiert, sobald ein -messbarer Ladestrom fließt. Beim Abstecken werden die erkannten Phasen -zurückgesetzt. Die adaptive Maximalstrom- und Ladeenderkennung bleibt aktiv. +## Gemeinsames Soll-/Ist-Modell -Typ 4 bleibt unverändert. +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 bei Typ 1 bis 4 unter 6 A, wird +`Auto fertig geladen` gesetzt und nur `[0]` angeboten. Easee verwendet dafür +ausschließlich Observation 109. Eine Sollvorgabe von 0 W setzt die Zähler +zurück und kann weder eine Strombegrenzung noch ein Ladeende auslösen. Beim Update bestehender Instanzen wird die frühere Eigenschaft `Token_Easee` aus Kompatibilitätsgründen intern beibehalten, aber nicht mehr verwendet oder diff --git a/Ladestation/module.php b/Ladestation/module.php index b711506..95bd86b 100644 --- a/Ladestation/module.php +++ b/Ladestation/module.php @@ -5,6 +5,10 @@ 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() { @@ -33,6 +37,7 @@ 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); @@ -102,6 +107,19 @@ 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 @@ -272,28 +290,43 @@ class Ladestation extends IPSModule $this->SetTimerInterval("Timer_Refresh_Token", 0); $stationType = $this->ReadPropertyInteger("Ladestation"); - // Bestehende Pico-Instanzen können aus der bisherigen Erkennung einen - // ungültigen Grenzwert von 2.5 A und dadurch "voll" gespeichert haben. - // Diese Altlast wird beim ersten Laden der korrigierten Version einmalig - // zurückgesetzt. + // Einmalige Migration aus den bisherigen stationsspezifischen + // Lernalgorithmen in das gemeinsame Soll-/Ist-Modell. if ( - $stationType === 3 && - $this->GetBuffer("SmartMeFullDetectionVersion") !== "4" + $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("SmartMeChargingEnabled", "0"); - $this->SetBuffer("SmartMeLearnedMaxCurrent", "0"); $this->SetBuffer( - "SmartMeStationMaxCurrent", + "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("SmartMeFullDetectionVersion", "4"); + $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); @@ -495,9 +528,264 @@ class Ladestation extends IPSModule private function UsesAdaptiveCurrentDetection(int $stationType): bool { - // Easee (5/6) liefert den Ladeendestatus direkt über das Gateway. - // Alle anderen Typen benötigen weiterhin die Erkennung über Soll-/Istleistung. - return in_array($stationType, [1, 2, 3, 4], true); + return in_array($stationType, [1, 2, 3, 4, 5, 6], true); + } + + private function HasReliableFullStatus(int $stationType): bool + { + // Easee meldet das Ladeende eindeutig über Observation 109. + return in_array($stationType, [5, 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") + ); + $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->HasReliableFullStatus($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") + ); + + $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && + $ladebereit && + $this->GetValue("Car_detected") && + !$this->GetValue("Car_is_full") && + $this->GetValue("Idle") && + ($stationType === 4 || + $this->GetBuffer("AdaptivePhaseKnown") === "1") && + $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 = abs( + (float)$this->GetValue("Ladeleistung_Effektiv") + ) / $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) { + // Bei Easee hat der eindeutige API-Ladeendestatus Vorrang. Ein + // Kommunikations- oder Freigabefehler darf dort kein Ladeende + // simulieren. + if ($this->HasReliableFullStatus($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; } private function ApplyEaseeGatewayResponse($response) @@ -519,7 +807,7 @@ class Ladestation extends IPSModule } $updated = (int)($response["state"]["updated"] ?? time()); - foreach ([47, 109, 110, 120, 182, 183, 184, 185] as $observationID) { + foreach ([47, 104, 109, 110, 120, 182, 183, 184, 185] as $observationID) { $key = (string)$observationID; if (array_key_exists($key, $response["state"])) { $this->StoreEaseeObservation( @@ -533,7 +821,7 @@ class Ladestation extends IPSModule private function StoreEaseeObservation(int $observationID, $value, int $timestamp) { - if (!in_array($observationID, [47, 109, 110, 120, 182, 183, 184, 185], true)) { + if (!in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true)) { return; } @@ -559,6 +847,7 @@ class Ladestation extends IPSModule $this->SetValue("Fahrzeugstatus", (int)$value); } elseif ($observationID === 120) { $this->SetValue("Ladeleistung_Effektiv", round((float)$value * 1000)); + $this->StoreAdaptiveMeasurement($timestamp); } } @@ -586,18 +875,13 @@ 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 = []; } - $this->SetValue("Pending_Counter", 0); - - $this->DetectEaseePhases($state); - $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); + $isOnePhase = $this->DetectEaseePhases($state); // Observation 47 ist der nichtflüchtige maximale Ladestrom der // Station. Die lokal konfigurierte Obergrenze bleibt zusätzlich aktiv. @@ -605,39 +889,26 @@ class Ladestation extends IPSModule if (array_key_exists("47", $state) && (float)$state["47"] > 0) { $maxCurrent = min($maxCurrent, (float)$state["47"]); } - $this->SetValue("Max_Current", $maxCurrent); + // 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"]); + } - if (!$carDetected) { + $chargerOpMode = (int)($state["109"] ?? -1); + if (!$carDetected && $chargerOpMode !== 1) { + // Einen vorübergehenden Fehlerstatus nicht als Ausstecken werten. $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); - - $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); - } - + $this->SetValue("Pending_Counter", 0); return false; } - $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; + return $this->ApplyDirectStationDetection( + $carDetected, + $chargerOpMode === 4, + $isOnePhase, + $maxCurrent + ); } private function DetectEaseePhases(array $state) @@ -650,7 +921,7 @@ class Ladestation extends IPSModule array_key_exists("120", $state) && abs((float)$state["120"]) < 0.1 ) { - return; + return null; } $activePhaseConductors = 0; @@ -661,24 +932,25 @@ class Ladestation extends IPSModule } if ($activePhaseConductors === 0) { - return; + return null; } // Ein explizit einphasiger Easee-Ausgang ist auch in IT-Netzen // einphasig, obwohl dort Strom auf zwei Leitern gemessen wird. if ($outputPhase >= 10 && $outputPhase <= 15) { - $this->SetValue("Is_1_ph", true); - return; + return true; } // 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) { - $this->SetValue("Is_1_ph", true); + return true; } elseif ($activePhaseConductors >= 2) { - $this->SetValue("Is_1_ph", false); + return false; } + + return null; } private function ReadJsonEndpoint(string $url, bool $useAuthentication = false) @@ -738,20 +1010,19 @@ class Ladestation extends IPSModule bool $carDetected, bool $carIsFull, $isOnePhase, - float $maxCurrent, - bool $resetPendingCounter = true + float $maxCurrent ): bool { $wasDetected = $this->GetValue("Car_detected"); $wasOnePhase = $this->GetValue("Is_1_ph"); - $currentMax = (float)$this->GetValue("Max_Current"); - - if ($resetPendingCounter) { - $this->SetValue("Pending_Counter", 0); - } + $wasFull = $this->GetValue("Car_is_full"); + $referenceMaxCurrent = $this->SetAdaptiveReferenceMaxCurrent( + $maxCurrent + ); if (!$carDetected) { - $this->SetValue("Pending_Counter", 0); - $this->SetValue("Max_Current", $maxCurrent); + $this->ResetAdaptiveLearning($referenceMaxCurrent); + $this->SetBuffer("AdaptivePhaseKnown", "0"); + $this->SetBuffer("AdaptivePhaseCounter", "0"); $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); $this->SetValue("Leistung_Delta", 0); @@ -766,19 +1037,39 @@ 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 = $wasOnePhase !== $this->GetValue("Is_1_ph"); - if (!$wasDetected || $phaseChanged || $currentMax <= 0) { - $this->SetValue("Max_Current", $maxCurrent); + $phaseChanged = $wasDetected && + is_bool($isOnePhase) && + $wasOnePhase !== $this->GetValue("Is_1_ph"); + if (!$wasDetected || $phaseChanged) { + $this->ResetAdaptiveLearning($referenceMaxCurrent); } else { - // Einen vom Soll-/Istvergleich gelernten Fahrzeuggrenzwert nicht - // bei jedem API-Abruf wieder mit dem Stationsgrenzwert überschreiben. - $this->SetValue("Max_Current", min($currentMax, $maxCurrent)); + $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); + if ($this->HasReliableFullStatus($this->ReadPropertyInteger("Ladestation"))) { + $this->SetValue("Car_is_full", $carIsFull); + } else { + $this->SetValue( + "Car_is_full", + $carIsFull || ($wasDetected && !$phaseChanged && $wasFull) + ); + } if (!$wasDetected || $phaseChanged) { if ($this->GetValue("Is_1_ph")) { @@ -805,6 +1096,10 @@ 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) { @@ -823,8 +1118,14 @@ class Ladestation extends IPSModule return (int)$data["alw"] === 1; } - if ($stationType === 2 && array_key_exists("frc", $data)) { - return (int)$data["frc"] === 2; + if (array_key_exists("frc", $data)) { + $forceState = (int)$data["frc"]; + if ($forceState === 1) { + return false; + } + if ($forceState === 2) { + return true; + } } if (array_key_exists("alw", $data)) { @@ -834,46 +1135,6 @@ 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 - ) { - 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 - ) { - return false; - } - - // Der zentrale Soll-/Istvergleich setzt Max_Current nach 90 Sekunden. - // Unter 6 A entspricht dies der bisherigen Erkennung "Auto geladen". - $learnedMaxCurrent = (float)$this->GetValue("Max_Current"); - return $this->GetValue("Car_detected") && - $learnedMaxCurrent > 0 && - $learnedMaxCurrent < 6; - } - private function DetectGoECar(int $stationType): bool { $path = $stationType === 1 ? "/mqtt?payload=" : "/api/status"; @@ -916,28 +1177,18 @@ class Ladestation extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); + $this->StoreAdaptiveMeasurement(); } } $carDetected = in_array($carState, [2, 3, 4, 5], true); $stationMaxCurrent = $this->GetGoEMaxCurrent($data); - $wasFull = $this->GetValue("Car_is_full"); - $carIsFull = $carDetected && $this->DetectGoEFullCharge( - $data, - $stationType, - $carState, - $chargingPowerWatts - ); - if ($wasFull && !$carIsFull) { - $this->SetValue("Max_Current", $stationMaxCurrent); - } return $this->ApplyDirectStationDetection( $carDetected, - $carIsFull, + false, $isOnePhase, - $stationMaxCurrent, - false + $stationMaxCurrent ); } @@ -981,6 +1232,7 @@ class Ladestation extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); + $this->StoreAdaptiveMeasurement(); } // Die Pico-API unterscheidet zwischen der extern vorgegebenen @@ -1036,8 +1288,6 @@ class Ladestation extends IPSModule foreach ([ "MaxStationCurrent", "maxStationCurrent", - "MaxLoadmanagementGroupCurrent", - "maxLoadmanagementGroupCurrent", ] as $key) { if (isset($chargingData[$key]) && is_numeric($chargingData[$key])) { $candidate = (float)$chargingData[$key]; @@ -1047,45 +1297,13 @@ class Ladestation extends IPSModule } } - $this->SetBuffer("SmartMeStationMaxCurrent", (string)$maxCurrent); - $carDetected = in_array($stationState, [2, 3, 4, 6], true); - $wasDetected = $this->GetValue("Car_detected"); - $wasOnePhase = $this->GetValue("Is_1_ph"); - $phaseChanged = is_bool($isOnePhase) && - $wasDetected && - $wasOnePhase !== $isOnePhase; - - // Beim Abstecken, bei einem neuen Fahrzeug oder bei einem echten - // Phasenwechsel wird die zuvor gelernte Fahrzeug-/Kabelgrenze verworfen. - if (!$carDetected || !$wasDetected || $phaseChanged) { - $this->SetBuffer("SmartMeLearnedMaxCurrent", "0"); - } - - $learnedMaxCurrent = (float)$this->GetBuffer( - "SmartMeLearnedMaxCurrent" - ); - $effectiveMaxCurrent = $maxCurrent; - if ($learnedMaxCurrent >= $minStationCurrent) { - $effectiveMaxCurrent = min($maxCurrent, $learnedMaxCurrent); - } - - $wasFull = $this->GetValue("Car_is_full"); - $carIsCharging = is_numeric($chargingPowerWatts) && - abs((float)$chargingPowerWatts) >= 100; - $carIsFull = $carDetected && $wasFull && !$carIsCharging; - - // Die API-Grenze bleibt die absolute Obergrenze. Eine bestätigte - // Fahrzeug-/Kabelgrenze darf darunter liegen und bleibt erhalten, - // solange dasselbe Fahrzeug mit derselben Phasenart angeschlossen ist. - $this->SetValue("Max_Current", $effectiveMaxCurrent); return $this->ApplyDirectStationDetection( $carDetected, - $carIsFull, + false, $isOnePhase, - $effectiveMaxCurrent, - false + $maxCurrent ); } @@ -1158,11 +1376,13 @@ 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); } @@ -1399,19 +1619,26 @@ class Ladestation extends IPSModule public function Calc_Max_Current(bool $is_1_ph) { - $maxCurrent = 32; - if($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 = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / 230); - } - else{ + $this->SetBuffer("AdaptiveLearnedMaxCurrent", (string)$maxCurrent); + $this->SetValue("Max_Current", $maxCurrent); - $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / (1.71*400)); + if ( + $maxCurrent < 6.0 && + !$this->HasReliableFullStatus( + $this->ReadPropertyInteger("Ladestation") + ) + ) { + $this->SetValue("Car_is_full", true); } - 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) @@ -1541,166 +1768,101 @@ 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) { - - // Ü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")); + $this->SetValue("Pending_Counter", 0); + if ( + $this->GetValue("Car_is_full") || + !$this->GetValue("Ladebereit") + ) { + $this->ResetTimer(); + $this->SetValue("PowerSteps", json_encode([0])); + return [0]; + } - return; - }else{ - $this->SetValue("Leistung_Delta", 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")]; } - // Aktuelle Properties abrufen - $ladebereit = GetValue($this->GetIDForIdent("Ladebereit")); - $solarladen = GetValue($this->GetIDForIdent("Solarladen")); - - - // Peak-Wert speichern + $this->SetValue("Leistung_Delta", 0); + $ladebereit = (bool)$this->GetValue("Ladebereit"); + $solarladen = (bool)$this->GetValue("Solarladen"); $this->SetValue("Peak", $Peak); - // Array für die Powersteps initialisieren - $powerSteps = []; - - // 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 $stationType = $this->ReadPropertyInteger("Ladestation"); - if ($this->UsesAdaptiveCurrentDetection($stationType)) { - $requestedPower = max(0, (float)$this->GetValue("Aktuelle_Leistung")); - $actualPower = abs((float)$this->GetValue("Ladeleistung_Effektiv")); - $availableSteps = $this->Get_Array_From_Current( - $this->GetValue("Is_1_ph"), - $this->GetValue("Max_Current"), - $this->GetValue("Aktuelle_Leistung"), - $this->GetValue("IsTimerActive_Null_Timer") + $phaseWasDetected = $this->UpdateFallbackPhaseDetection( + $stationType, + $ladebereit + ); + + // 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 ); - $maxAvailablePower = count($availableSteps) > 0 - ? max($availableSteps) - : 0; - $maxPowerStep = count($powerSteps) > 0 ? max($powerSteps) : 0; - $picoAtMaximumRequest = $stationType !== 3 || - ( - $maxAvailablePower > 0 && - $requestedPower >= (0.95 * $maxAvailablePower) - ); - $picoCanEvaluate = $stationType !== 3 || - $actualPower < 100 || - $picoAtMaximumRequest; - $goEEnabled = !in_array($stationType, [1, 2], true) || - $this->GetBuffer("GoEChargingEnabledFromStatus") === "1"; - $smartMeEnabled = $stationType !== 3 || - $this->GetBuffer("SmartMeChargingEnabled") === "1"; - $canEvaluate = $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $requestedPower > 0 && - $maxPowerStep > 0 && - $goEEnabled && - $smartMeEnabled && - $picoCanEvaluate; - - $requestedMismatch = $requestedPower > (1.11 * $actualPower); - $availableMismatch = $maxAvailablePower < (1.11 * $actualPower); - - if ($canEvaluate && ($requestedMismatch || $availableMismatch)) { - $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) { - $this->SetValue("Pending_Counter", 0); - if ($requestedMismatch) { - if ($stationType === 3) { - // Smart-Me liefert keinen eindeutigen Status "Auto - // voll". Der berechnete Fahrzeugwert dient hier nur - // zur Ladeende-Erkennung und wird nicht als neue - // dauerhafte Stationsobergrenze gespeichert. - $learnedMaxCurrent = 2.5 + ( - $actualPower / - ($this->GetValue("Is_1_ph") ? 230 : 1.71 * 400) - ); - - if ( - $this->GetBuffer("SmartMeChargingEnabled") === "1" && - $learnedMaxCurrent > 0 && - $learnedMaxCurrent < 6 - ) { - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - } elseif ($picoAtMaximumRequest) { - $minStationCurrent = max( - 1.0, - (float)$this->GetBuffer( - "SmartMeMinStationCurrent" - ) - ); - $phaseDivisor = $this->GetValue("Is_1_ph") - ? 230 - : 1.71 * 400; - $actualCurrent = $actualPower / $phaseDivisor; - - // Erst eine stabile Stromaufnahme mindestens - // nahe dem Stationsminimum darf als Kabel- oder - // Fahrzeuggrenze gespeichert werden. - if ($actualCurrent >= 0.9 * $minStationCurrent) { - $stationMaxCurrent = max( - $minStationCurrent, - (float)$this->GetBuffer( - "SmartMeStationMaxCurrent" - ) - ); - $learnedMaxCurrent = min( - $stationMaxCurrent, - $learnedMaxCurrent - ); - $this->SetBuffer( - "SmartMeLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue( - "Max_Current", - $learnedMaxCurrent - ); - } - } - } else { - $this->Calc_Max_Current($this->GetValue("Is_1_ph")); - } - } - } - } else { - $this->SetValue("Pending_Counter", 0); - } - } else { - $this->SetValue("Pending_Counter", 0); } - + + $powerSteps = $this->BuildPowerSteps( + $Peak, + $ladebereit, + $solarladen + ); + $this->SetValue("PowerSteps", json_encode($powerSteps)); return $powerSteps; } diff --git a/Ladestation_v2/README.md b/Ladestation_v2/README.md index 22d1226..6a7b3f4 100644 --- a/Ladestation_v2/README.md +++ b/Ladestation_v2/README.md @@ -20,20 +20,19 @@ 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 -Bei Easee erfolgt die Fahrzeug- und Phasenerkennung ohne den zeitbasierten -`Pending_Counter`. Der maximale Ladestrom wird aus Observation 47 übernommen -und zusätzlich durch `Max_Current_abs` begrenzt. Die Typen 1 bis 4 verwenden -weiterhin die adaptive Soll-/Istleistungserkennung mit dem `Pending_Counter`. 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. +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. @@ -46,29 +45,32 @@ Die Typen 1 bis 4 benötigen kein Easee-Gateway. 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 -die Stations-, Kabel- und Modulgrenze begrenzt. - -Ist die angeforderte Ladeleistung während 90 Sekunden um den Faktor `1.11` -höher als die tatsächliche Leistung, wird der fahrzeugseitige Maximalstrom -aus dem Istwert neu berechnet. Fällt er unter 6 A, wird `Auto fertig geladen` -gesetzt. Bei einer durch das Modul angeforderten 0-W-Pause wird der -`Pending_Counter` zurückgesetzt. Zusätzlich muss bei Go-E die Ladung -freigegeben sein und der Fahrzeugstatus 4 anliegen. +`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. Der fahrzeugseitige Maximalstrom und das Ladeende -werden weiterhin über den 90-Sekunden-Soll-/Istvergleich ermittelt. +Pico-Ladedaten übernommen. -Bei Typ 1 bis 3 entfällt nur die frühere zeitbasierte Phasenerkennung. Die -Phasenzahl wird direkt aus den API-Messwerten aktualisiert, sobald ein -messbarer Ladestrom fließt. Beim Abstecken werden die erkannten Phasen -zurückgesetzt. Die adaptive Maximalstrom- und Ladeenderkennung bleibt aktiv. +## Gemeinsames Soll-/Ist-Modell -Typ 4 bleibt unverändert. +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 bei Typ 1 bis 4 unter 6 A, wird +`Auto fertig geladen` gesetzt und nur `[0]` angeboten. Easee verwendet dafür +ausschließlich Observation 109. Eine Sollvorgabe von 0 W setzt die Zähler +zurück und kann weder eine Strombegrenzung noch ein Ladeende auslösen. ## Einrichtung diff --git a/Ladestation_v2/module.php b/Ladestation_v2/module.php index 0dea035..b076101 100644 --- a/Ladestation_v2/module.php +++ b/Ladestation_v2/module.php @@ -5,6 +5,10 @@ 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() { @@ -30,6 +34,7 @@ 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); @@ -99,6 +104,19 @@ 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); @@ -282,28 +300,43 @@ class Ladestation_v2 extends IPSModule $this->SetTimerInterval("Timer_Do_UserCalc_EVC",$this->ReadPropertyInteger("Interval")*1000); $stationType = $this->ReadPropertyInteger("Ladestation"); - // Bestehende Pico-Instanzen können aus der bisherigen Erkennung einen - // ungültigen Grenzwert von 2.5 A und dadurch "voll" gespeichert haben. - // Diese Altlast wird beim ersten Laden der korrigierten Version einmalig - // zurückgesetzt. + // Einmalige Migration aus den bisherigen stationsspezifischen + // Lernalgorithmen in das gemeinsame Soll-/Ist-Modell. if ( - $stationType === 3 && - $this->GetBuffer("SmartMeFullDetectionVersion") !== "4" + $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("SmartMeChargingEnabled", "0"); - $this->SetBuffer("SmartMeLearnedMaxCurrent", "0"); $this->SetBuffer( - "SmartMeStationMaxCurrent", + "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("SmartMeFullDetectionVersion", "4"); + $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); @@ -515,9 +548,264 @@ class Ladestation_v2 extends IPSModule private function UsesAdaptiveCurrentDetection(int $stationType): bool { - // Easee (5/6) liefert den Ladeendestatus direkt über das Gateway. - // Alle anderen Typen benötigen weiterhin die Erkennung über Soll-/Istleistung. - return in_array($stationType, [1, 2, 3, 4], true); + return in_array($stationType, [1, 2, 3, 4, 5, 6], true); + } + + private function HasReliableFullStatus(int $stationType): bool + { + // Easee meldet das Ladeende eindeutig über Observation 109. + return in_array($stationType, [5, 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") + ); + $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->HasReliableFullStatus($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") + ); + + $canEvaluate = $this->UsesAdaptiveCurrentDetection($stationType) && + $ladebereit && + $this->GetValue("Car_detected") && + !$this->GetValue("Car_is_full") && + $this->GetValue("Idle") && + ($stationType === 4 || + $this->GetBuffer("AdaptivePhaseKnown") === "1") && + $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 = abs( + (float)$this->GetValue("Ladeleistung_Effektiv") + ) / $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) { + // Bei Easee hat der eindeutige API-Ladeendestatus Vorrang. Ein + // Kommunikations- oder Freigabefehler darf dort kein Ladeende + // simulieren. + if ($this->HasReliableFullStatus($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; } private function ApplyEaseeGatewayResponse($response) @@ -539,7 +827,7 @@ class Ladestation_v2 extends IPSModule } $updated = (int)($response["state"]["updated"] ?? time()); - foreach ([47, 109, 110, 120, 182, 183, 184, 185] as $observationID) { + foreach ([47, 104, 109, 110, 120, 182, 183, 184, 185] as $observationID) { $key = (string)$observationID; if (array_key_exists($key, $response["state"])) { $this->StoreEaseeObservation( @@ -553,7 +841,7 @@ class Ladestation_v2 extends IPSModule private function StoreEaseeObservation(int $observationID, $value, int $timestamp) { - if (!in_array($observationID, [47, 109, 110, 120, 182, 183, 184, 185], true)) { + if (!in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true)) { return; } @@ -579,6 +867,7 @@ class Ladestation_v2 extends IPSModule $this->SetValue("Fahrzeugstatus", (int)$value); } elseif ($observationID === 120) { $this->SetValue("Ladeleistung_Effektiv", round((float)$value * 1000)); + $this->StoreAdaptiveMeasurement($timestamp); } } @@ -606,18 +895,13 @@ 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 = []; } - $this->SetValue("Pending_Counter", 0); - - $this->DetectEaseePhases($state); - $phaseChanged = $wasOnePhase !== $this->GetValue("Is_1_ph"); + $isOnePhase = $this->DetectEaseePhases($state); // Observation 47 ist der nichtflüchtige maximale Ladestrom der // Station. Die lokal konfigurierte Obergrenze bleibt zusätzlich aktiv. @@ -625,39 +909,26 @@ class Ladestation_v2 extends IPSModule if (array_key_exists("47", $state) && (float)$state["47"] > 0) { $maxCurrent = min($maxCurrent, (float)$state["47"]); } - $this->SetValue("Max_Current", $maxCurrent); + // 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"]); + } - if (!$carDetected) { + $chargerOpMode = (int)($state["109"] ?? -1); + if (!$carDetected && $chargerOpMode !== 1) { + // Einen vorübergehenden Fehlerstatus nicht als Ausstecken werten. $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); - - $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); - } - + $this->SetValue("Pending_Counter", 0); return false; } - $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; + return $this->ApplyDirectStationDetection( + $carDetected, + $chargerOpMode === 4, + $isOnePhase, + $maxCurrent + ); } private function DetectEaseePhases(array $state) @@ -670,7 +941,7 @@ class Ladestation_v2 extends IPSModule array_key_exists("120", $state) && abs((float)$state["120"]) < 0.1 ) { - return; + return null; } $activePhaseConductors = 0; @@ -681,24 +952,25 @@ class Ladestation_v2 extends IPSModule } if ($activePhaseConductors === 0) { - return; + return null; } // Ein explizit einphasiger Easee-Ausgang ist auch in IT-Netzen // einphasig, obwohl dort Strom auf zwei Leitern gemessen wird. if ($outputPhase >= 10 && $outputPhase <= 15) { - $this->SetValue("Is_1_ph", true); - return; + return true; } // 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) { - $this->SetValue("Is_1_ph", true); + return true; } elseif ($activePhaseConductors >= 2) { - $this->SetValue("Is_1_ph", false); + return false; } + + return null; } private function ReadJsonEndpoint(string $url, bool $useAuthentication = false) @@ -758,20 +1030,19 @@ class Ladestation_v2 extends IPSModule bool $carDetected, bool $carIsFull, $isOnePhase, - float $maxCurrent, - bool $resetPendingCounter = true + float $maxCurrent ): bool { $wasDetected = $this->GetValue("Car_detected"); $wasOnePhase = $this->GetValue("Is_1_ph"); - $currentMax = (float)$this->GetValue("Max_Current"); - - if ($resetPendingCounter) { - $this->SetValue("Pending_Counter", 0); - } + $wasFull = $this->GetValue("Car_is_full"); + $referenceMaxCurrent = $this->SetAdaptiveReferenceMaxCurrent( + $maxCurrent + ); if (!$carDetected) { - $this->SetValue("Pending_Counter", 0); - $this->SetValue("Max_Current", $maxCurrent); + $this->ResetAdaptiveLearning($referenceMaxCurrent); + $this->SetBuffer("AdaptivePhaseKnown", "0"); + $this->SetBuffer("AdaptivePhaseCounter", "0"); $this->SetValue("Car_detected", false); $this->SetValue("Car_is_full", false); $this->SetValue("Leistung_Delta", 0); @@ -786,19 +1057,39 @@ 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 = $wasOnePhase !== $this->GetValue("Is_1_ph"); - if (!$wasDetected || $phaseChanged || $currentMax <= 0) { - $this->SetValue("Max_Current", $maxCurrent); + $phaseChanged = $wasDetected && + is_bool($isOnePhase) && + $wasOnePhase !== $this->GetValue("Is_1_ph"); + if (!$wasDetected || $phaseChanged) { + $this->ResetAdaptiveLearning($referenceMaxCurrent); } else { - // Einen vom Soll-/Istvergleich gelernten Fahrzeuggrenzwert nicht - // bei jedem API-Abruf wieder mit dem Stationsgrenzwert überschreiben. - $this->SetValue("Max_Current", min($currentMax, $maxCurrent)); + $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); + if ($this->HasReliableFullStatus($this->ReadPropertyInteger("Ladestation"))) { + $this->SetValue("Car_is_full", $carIsFull); + } else { + $this->SetValue( + "Car_is_full", + $carIsFull || ($wasDetected && !$phaseChanged && $wasFull) + ); + } if (!$wasDetected || $phaseChanged) { if ($this->GetValue("Is_1_ph")) { @@ -825,6 +1116,10 @@ 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) { @@ -843,8 +1138,14 @@ class Ladestation_v2 extends IPSModule return (int)$data["alw"] === 1; } - if ($stationType === 2 && array_key_exists("frc", $data)) { - return (int)$data["frc"] === 2; + if (array_key_exists("frc", $data)) { + $forceState = (int)$data["frc"]; + if ($forceState === 1) { + return false; + } + if ($forceState === 2) { + return true; + } } if (array_key_exists("alw", $data)) { @@ -854,46 +1155,6 @@ 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 - ) { - 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 - ) { - return false; - } - - // Der zentrale Soll-/Istvergleich setzt Max_Current nach 90 Sekunden. - // Unter 6 A entspricht dies der bisherigen Erkennung "Auto geladen". - $learnedMaxCurrent = (float)$this->GetValue("Max_Current"); - return $this->GetValue("Car_detected") && - $learnedMaxCurrent > 0 && - $learnedMaxCurrent < 6; - } - private function DetectGoECar(int $stationType): bool { $path = $stationType === 1 ? "/mqtt?payload=" : "/api/status"; @@ -936,28 +1197,18 @@ class Ladestation_v2 extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); + $this->StoreAdaptiveMeasurement(); } } $carDetected = in_array($carState, [2, 3, 4, 5], true); $stationMaxCurrent = $this->GetGoEMaxCurrent($data); - $wasFull = $this->GetValue("Car_is_full"); - $carIsFull = $carDetected && $this->DetectGoEFullCharge( - $data, - $stationType, - $carState, - $chargingPowerWatts - ); - if ($wasFull && !$carIsFull) { - $this->SetValue("Max_Current", $stationMaxCurrent); - } return $this->ApplyDirectStationDetection( $carDetected, - $carIsFull, + false, $isOnePhase, - $stationMaxCurrent, - false + $stationMaxCurrent ); } @@ -1001,6 +1252,7 @@ class Ladestation_v2 extends IPSModule "Ladeleistung_Effektiv", round($chargingPowerWatts) ); + $this->StoreAdaptiveMeasurement(); } // Die Pico-API unterscheidet zwischen der extern vorgegebenen @@ -1056,8 +1308,6 @@ class Ladestation_v2 extends IPSModule foreach ([ "MaxStationCurrent", "maxStationCurrent", - "MaxLoadmanagementGroupCurrent", - "maxLoadmanagementGroupCurrent", ] as $key) { if (isset($chargingData[$key]) && is_numeric($chargingData[$key])) { $candidate = (float)$chargingData[$key]; @@ -1067,45 +1317,13 @@ class Ladestation_v2 extends IPSModule } } - $this->SetBuffer("SmartMeStationMaxCurrent", (string)$maxCurrent); - $carDetected = in_array($stationState, [2, 3, 4, 6], true); - $wasDetected = $this->GetValue("Car_detected"); - $wasOnePhase = $this->GetValue("Is_1_ph"); - $phaseChanged = is_bool($isOnePhase) && - $wasDetected && - $wasOnePhase !== $isOnePhase; - - // Beim Abstecken, bei einem neuen Fahrzeug oder bei einem echten - // Phasenwechsel wird die zuvor gelernte Fahrzeug-/Kabelgrenze verworfen. - if (!$carDetected || !$wasDetected || $phaseChanged) { - $this->SetBuffer("SmartMeLearnedMaxCurrent", "0"); - } - - $learnedMaxCurrent = (float)$this->GetBuffer( - "SmartMeLearnedMaxCurrent" - ); - $effectiveMaxCurrent = $maxCurrent; - if ($learnedMaxCurrent >= $minStationCurrent) { - $effectiveMaxCurrent = min($maxCurrent, $learnedMaxCurrent); - } - - $wasFull = $this->GetValue("Car_is_full"); - $carIsCharging = is_numeric($chargingPowerWatts) && - abs((float)$chargingPowerWatts) >= 100; - $carIsFull = $carDetected && $wasFull && !$carIsCharging; - - // Die API-Grenze bleibt die absolute Obergrenze. Eine bestätigte - // Fahrzeug-/Kabelgrenze darf darunter liegen und bleibt erhalten, - // solange dasselbe Fahrzeug mit derselben Phasenart angeschlossen ist. - $this->SetValue("Max_Current", $effectiveMaxCurrent); return $this->ApplyDirectStationDetection( $carDetected, - $carIsFull, + false, $isOnePhase, - $effectiveMaxCurrent, - false + $maxCurrent ); } @@ -1178,11 +1396,13 @@ 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); } @@ -1412,19 +1632,26 @@ class Ladestation_v2 extends IPSModule public function Calc_Max_Current(bool $is_1_ph) { - $maxCurrent = 32; - if($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 = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / 230); - } - else{ + $this->SetBuffer("AdaptiveLearnedMaxCurrent", (string)$maxCurrent); + $this->SetValue("Max_Current", $maxCurrent); - $maxCurrent = 2.5 + ($this->GetValue("Ladeleistung_Effektiv") / (1.71*400)); + if ( + $maxCurrent < 6.0 && + !$this->HasReliableFullStatus( + $this->ReadPropertyInteger("Ladestation") + ) + ) { + $this->SetValue("Car_is_full", true); } - 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) @@ -1554,166 +1781,101 @@ 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) { - - // Ü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")); + $this->SetValue("Pending_Counter", 0); + if ( + $this->GetValue("Car_is_full") || + !$this->GetValue("Ladebereit") + ) { + $this->ResetTimer(); + $this->SetValue("PowerSteps", json_encode([0])); + return [0]; + } - return; - }else{ - $this->SetValue("Leistung_Delta", 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")]; } - // Aktuelle Properties abrufen - $ladebereit = GetValue($this->GetIDForIdent("Ladebereit")); - $solarladen = GetValue($this->GetIDForIdent("Solarladen")); - - - // Peak-Wert speichern + $this->SetValue("Leistung_Delta", 0); + $ladebereit = (bool)$this->GetValue("Ladebereit"); + $solarladen = (bool)$this->GetValue("Solarladen"); $this->SetValue("Peak", $Peak); - // Array für die Powersteps initialisieren - $powerSteps = []; - - // 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 $stationType = $this->ReadPropertyInteger("Ladestation"); - if ($this->UsesAdaptiveCurrentDetection($stationType)) { - $requestedPower = max(0, (float)$this->GetValue("Aktuelle_Leistung")); - $actualPower = abs((float)$this->GetValue("Ladeleistung_Effektiv")); - $availableSteps = $this->Get_Array_From_Current( - $this->GetValue("Is_1_ph"), - $this->GetValue("Max_Current"), - $this->GetValue("Aktuelle_Leistung"), - $this->GetValue("IsTimerActive_Null_Timer") + $phaseWasDetected = $this->UpdateFallbackPhaseDetection( + $stationType, + $ladebereit + ); + + // 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 ); - $maxAvailablePower = count($availableSteps) > 0 - ? max($availableSteps) - : 0; - $maxPowerStep = count($powerSteps) > 0 ? max($powerSteps) : 0; - $picoAtMaximumRequest = $stationType !== 3 || - ( - $maxAvailablePower > 0 && - $requestedPower >= (0.95 * $maxAvailablePower) - ); - $picoCanEvaluate = $stationType !== 3 || - $actualPower < 100 || - $picoAtMaximumRequest; - $goEEnabled = !in_array($stationType, [1, 2], true) || - $this->GetBuffer("GoEChargingEnabledFromStatus") === "1"; - $smartMeEnabled = $stationType !== 3 || - $this->GetBuffer("SmartMeChargingEnabled") === "1"; - $canEvaluate = $ladebereit && - $this->GetValue("Car_detected") && - !$this->GetValue("Car_is_full") && - $requestedPower > 0 && - $maxPowerStep > 0 && - $goEEnabled && - $smartMeEnabled && - $picoCanEvaluate; - - $requestedMismatch = $requestedPower > (1.11 * $actualPower); - $availableMismatch = $maxAvailablePower < (1.11 * $actualPower); - - if ($canEvaluate && ($requestedMismatch || $availableMismatch)) { - $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) { - $this->SetValue("Pending_Counter", 0); - if ($requestedMismatch) { - if ($stationType === 3) { - // Smart-Me liefert keinen eindeutigen Status "Auto - // voll". Der berechnete Fahrzeugwert dient hier nur - // zur Ladeende-Erkennung und wird nicht als neue - // dauerhafte Stationsobergrenze gespeichert. - $learnedMaxCurrent = 2.5 + ( - $actualPower / - ($this->GetValue("Is_1_ph") ? 230 : 1.71 * 400) - ); - - if ( - $this->GetBuffer("SmartMeChargingEnabled") === "1" && - $learnedMaxCurrent > 0 && - $learnedMaxCurrent < 6 - ) { - $this->SetValue("Car_is_full", true); - $this->ResetTimer(); - $this->ResetNullTimer(); - } elseif ($picoAtMaximumRequest) { - $minStationCurrent = max( - 1.0, - (float)$this->GetBuffer( - "SmartMeMinStationCurrent" - ) - ); - $phaseDivisor = $this->GetValue("Is_1_ph") - ? 230 - : 1.71 * 400; - $actualCurrent = $actualPower / $phaseDivisor; - - // Erst eine stabile Stromaufnahme mindestens - // nahe dem Stationsminimum darf als Kabel- oder - // Fahrzeuggrenze gespeichert werden. - if ($actualCurrent >= 0.9 * $minStationCurrent) { - $stationMaxCurrent = max( - $minStationCurrent, - (float)$this->GetBuffer( - "SmartMeStationMaxCurrent" - ) - ); - $learnedMaxCurrent = min( - $stationMaxCurrent, - $learnedMaxCurrent - ); - $this->SetBuffer( - "SmartMeLearnedMaxCurrent", - (string)$learnedMaxCurrent - ); - $this->SetValue( - "Max_Current", - $learnedMaxCurrent - ); - } - } - } else { - $this->Calc_Max_Current($this->GetValue("Is_1_ph")); - } - } - } - } else { - $this->SetValue("Pending_Counter", 0); - } - } else { - $this->SetValue("Pending_Counter", 0); } - + + $powerSteps = $this->BuildPowerSteps( + $Peak, + $ladebereit, + $solarladen + ); + $this->SetValue("PowerSteps", json_encode($powerSteps)); return $powerSteps; }