From 97ce906c911cb8abdac0d86336f14b9c47ea510d Mon Sep 17 00:00:00 2001 From: DanielHaefliger Date: Thu, 6 Aug 2026 10:59:08 +0200 Subject: [PATCH] no message --- EaseeGateway/README.md | 5 ++ EaseeGateway/module.php | 122 ++++++++++++++++++++++++++++++++++++-- Ladestation_v2/module.php | 1 + 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/EaseeGateway/README.md b/EaseeGateway/README.md index 7e9b8a9..9e4e8ef 100644 --- a/EaseeGateway/README.md +++ b/EaseeGateway/README.md @@ -15,6 +15,11 @@ Bei 0 A setzt das Gateway das dynamische Stromlimit auf 0. Positive Werte werden direkt als neues dynamisches Stromlimit gesendet. Positive Vorgaben unter 6 A werden auf den technischen Mindestladestrom von 6 A angehoben. +Die Gültigkeitsdauer des dynamischen Stromlimits wird aus dem Berechnungsintervall +der Ladestation abgeleitet. Das Gateway sendet nur bei einer Sollwertänderung, +bei fehlender Bestätigung durch Observation 48 oder rechtzeitig vor Ablauf der +Gültigkeitsdauer erneut. Die Mindestgültigkeit beträgt zwei Minuten. + Der benötigte WebSocket-Client wird nach dem Speichern der Easee-Zugangsdaten automatisch erstellt und konfiguriert. Im WebSocket-Client muss keine URL eingetragen werden. diff --git a/EaseeGateway/module.php b/EaseeGateway/module.php index c4d25a1..d615d93 100644 --- a/EaseeGateway/module.php +++ b/EaseeGateway/module.php @@ -50,6 +50,7 @@ class EaseeGateway extends IPSModule $this->SetBuffer("Subscriptions", "{}"); $this->SetBuffer("SubscribedThisConnection", "{}"); $this->SetBuffer("PendingInvocations", "{}"); + $this->SetBuffer("CurrentCommandStates", "{}"); $this->SetValue("SubscriptionCount", 0); $this->SetConnected(false); @@ -210,7 +211,8 @@ class EaseeGateway extends IPSModule return json_encode( $this->SetDynamicChargerCurrent( $serialNumber, - (float)($request["amps"] ?? -1) + (float)($request["amps"] ?? -1), + max(1, (int)($request["pollIntervalSeconds"] ?? 5)) ) ); @@ -499,7 +501,7 @@ class EaseeGateway extends IPSModule if ( $serialNumber !== "" && $valueExists && - ($observationID === 109 || $observationID === 120) + ($observationID === 48 || $observationID === 109 || $observationID === 120) ) { $this->PublishObservation($serialNumber, $observationID, $value); } @@ -543,7 +545,11 @@ class EaseeGateway extends IPSModule ]); } - private function SetDynamicChargerCurrent(string $serialNumber, float $amps): array + private function SetDynamicChargerCurrent( + string $serialNumber, + float $amps, + int $pollIntervalSeconds + ): array { if ($serialNumber === "") { return ["success" => false, "error" => "Seriennummer fehlt"]; @@ -557,22 +563,128 @@ class EaseeGateway extends IPSModule $amps = 6; } + $timing = $this->CalculateCurrentCommandTiming($pollIntervalSeconds); + $states = $this->ReadBufferArray("CurrentCommandStates"); + $state = isset($states[$serialNumber]) && is_array($states[$serialNumber]) + ? $states[$serialNumber] + : []; + + $now = time(); + $lastAttempt = (int)($state["lastAttempt"] ?? 0); + $lastSuccess = (int)($state["lastSuccess"] ?? 0); + $desiredChanged = + !array_key_exists("amps", $state) || + abs((float)$state["amps"] - $amps) > 0.01 || + (int)($state["validityMinutes"] ?? 0) !== $timing["validityMinutes"]; + + $cache = $this->ReadAttributeArray("ObservationCache"); + $observedAmps = isset($cache[$serialNumber]["48"]) + ? (float)$cache[$serialNumber]["48"] + : null; + $confirmed = + $observedAmps !== null && + abs($observedAmps - $amps) <= 0.1; + + $refreshDue = + $lastSuccess <= 0 || + ($now - $lastSuccess) >= $timing["refreshAfterSeconds"]; + $retryDue = + !$confirmed && + ($lastAttempt <= 0 || ($now - $lastAttempt) >= $timing["retryAfterSeconds"]); + + $states[$serialNumber] = [ + "amps" => $amps, + "validityMinutes" => $timing["validityMinutes"], + "refreshAfterSeconds" => $timing["refreshAfterSeconds"], + "lastAttempt" => $lastAttempt, + "lastSuccess" => $lastSuccess, + ]; + + if (!$desiredChanged && !$refreshDue && !$retryDue) { + $this->SetBuffer("CurrentCommandStates", json_encode($states)); + $this->SendDebug( + "DynamicCurrent", + json_encode([ + "serialNumber" => $serialNumber, + "requestedAmps" => $requestedAmps, + "effectiveAmps" => $amps, + "observedAmps" => $observedAmps, + "confirmed" => $confirmed, + "command" => "skipped", + "refreshInSeconds" => max( + 0, + $timing["refreshAfterSeconds"] - ($now - $lastSuccess) + ), + ]), + 0 + ); + + return [ + "success" => true, + "skipped" => true, + "confirmed" => $confirmed, + "observedAmps" => $observedAmps, + ]; + } + $this->SendDebug( "DynamicCurrent", json_encode([ "serialNumber" => $serialNumber, "requestedAmps" => $requestedAmps, "effectiveAmps" => $amps, + "observedAmps" => $observedAmps, + "confirmed" => $confirmed, + "validityMinutes" => $timing["validityMinutes"], + "refreshAfterSeconds" => $timing["refreshAfterSeconds"], + "command" => $desiredChanged + ? "changed" + : ($refreshDue ? "refresh" : "retry"), ]), 0 ); - return $this->AuthorizedApiRequest( + $states[$serialNumber]["lastAttempt"] = $now; + $result = $this->AuthorizedApiRequest( "POST", "/api/chargers/" . rawurlencode($serialNumber) . "/commands/set_dynamic_charger_current", - json_encode(["amps" => $amps, "minutes" => 0]) + json_encode([ + "amps" => (int)round($amps), + "minutes" => $timing["validityMinutes"], + ]) ); + + if ($result["success"] ?? false) { + $states[$serialNumber]["lastSuccess"] = $now; + } + $this->SetBuffer("CurrentCommandStates", json_encode($states)); + + return $result; + } + + private function CalculateCurrentCommandTiming(int $pollIntervalSeconds): array + { + $pollIntervalSeconds = max(1, $pollIntervalSeconds); + + // Mindestens zwei Minuten Gültigkeit. Bei langen Abrufintervallen gilt + // der Sollwert mindestens vier Zyklen lang. + $validitySeconds = max(120, $pollIntervalSeconds * 4); + $validityMinutes = (int)ceil($validitySeconds / 60); + $actualValiditySeconds = $validityMinutes * 60; + + // Mindestens 30 Sekunden bzw. zwei Abrufzyklen Sicherheitsabstand. + $safetyMarginSeconds = max(30, $pollIntervalSeconds * 2); + $refreshAfterSeconds = max( + $pollIntervalSeconds, + $actualValiditySeconds - $safetyMarginSeconds + ); + + return [ + "validityMinutes" => $validityMinutes, + "refreshAfterSeconds" => $refreshAfterSeconds, + "retryAfterSeconds" => max(10, min(30, $pollIntervalSeconds * 2)), + ]; } private function AuthorizedApiRequest(string $method, string $path, string $body): array diff --git a/Ladestation_v2/module.php b/Ladestation_v2/module.php index e1d02a5..bab5b2d 100644 --- a/Ladestation_v2/module.php +++ b/Ladestation_v2/module.php @@ -574,6 +574,7 @@ class Ladestation_v2 extends IPSModule "action" => "SetDynamicChargerCurrent", "serialNumber" => strtoupper(trim($this->ReadPropertyString("Seriennummer"))), "amps" => $value, + "pollIntervalSeconds" => max(1, $this->ReadPropertyInteger("Interval")), ]); if (!is_array($response) || !($response["success"] ?? false)) {