diff --git a/EaseeGateway/README.md b/EaseeGateway/README.md new file mode 100644 index 0000000..00a0960 --- /dev/null +++ b/EaseeGateway/README.md @@ -0,0 +1,12 @@ +# Easee SignalR Gateway + +Das Gateway hält eine gemeinsame SignalR-Verbindung zum Easee-Konto und verteilt +die Observations 109 (Charger Op Mode) und 120 (Total Power) an alle verbundenen +Ladestation_v2-Instanzen. + +Benutzername und Passwort werden ausschließlich im Gateway eingetragen. Mehrere +Easee-Ladestationen desselben Kontos verwenden dasselbe Gateway. Für ein weiteres +Easee-Konto wird eine weitere Gateway-Instanz benötigt. + +Stromvorgaben werden von den Ladestationsinstanzen an das Gateway übergeben und +von dort über die Easee-REST-API gesendet. diff --git a/EaseeGateway/form.json b/EaseeGateway/form.json new file mode 100644 index 0000000..c23db72 --- /dev/null +++ b/EaseeGateway/form.json @@ -0,0 +1,52 @@ +{ + "elements": [ + { + "type": "CheckBox", + "name": "Active", + "caption": "Gateway aktiv" + }, + { + "type": "ValidationTextBox", + "name": "Username", + "caption": "Easee Benutzername" + }, + { + "type": "PasswordTextBox", + "name": "Password", + "caption": "Easee Passwort" + }, + { + "type": "CheckBox", + "name": "VerifyCertificate", + "caption": "TLS-Zertifikat prüfen" + }, + { + "type": "Label", + "caption": "Ein Gateway wird von allen Easee-Ladestationen desselben Kontos gemeinsam verwendet." + } + ], + "actions": [ + { + "type": "Button", + "caption": "Verbindung neu aufbauen", + "onClick": "IPS_RequestAction($id, \"Reconnect\", 0);" + } + ], + "status": [ + { + "code": 201, + "icon": "error", + "caption": "Easee-Zugangsdaten fehlen" + }, + { + "code": 202, + "icon": "error", + "caption": "Easee-Anmeldung fehlgeschlagen" + }, + { + "code": 203, + "icon": "error", + "caption": "SignalR-Verbindung fehlgeschlagen" + } + ] +} diff --git a/EaseeGateway/module.json b/EaseeGateway/module.json new file mode 100644 index 0000000..a604e57 --- /dev/null +++ b/EaseeGateway/module.json @@ -0,0 +1,21 @@ +{ + "id": "{7A1F1D4B-3E7D-4A3B-9E54-61C8C3B7F201}", + "name": "EaseeGateway", + "type": 2, + "vendor": "Belevo AG", + "aliases": [ + "Easee SignalR Gateway" + ], + "parentRequirements": [ + "{79827379-F36E-4ADA-8A95-5F8D1DC92FA9}" + ], + "childRequirements": [ + "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}" + ], + "implemented": [ + "{018EF6B5-AB94-40C6-AA53-46943E824ACF}", + "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}" + ], + "prefix": "EASEEGW", + "url": "" +} diff --git a/EaseeGateway/module.php b/EaseeGateway/module.php new file mode 100644 index 0000000..14213b5 --- /dev/null +++ b/EaseeGateway/module.php @@ -0,0 +1,717 @@ +RegisterPropertyBoolean("Active", true); + $this->RegisterPropertyString("Username", ""); + $this->RegisterPropertyString("Password", ""); + $this->RegisterPropertyBoolean("VerifyCertificate", true); + + $this->RegisterAttributeString("ObservationCache", "{}"); + + $this->RegisterVariableBoolean("Connected", "SignalR verbunden", "~Switch", false); + $this->RegisterVariableInteger("SubscriptionCount", "Abonnierte Ladestationen", "", 0); + $this->RegisterVariableString("LastError", "Letzter Fehler", "", 0); + + $this->RegisterTimer( + "MaintainConnectionTimer", + 0, + 'IPS_RequestAction(' . $this->InstanceID . ', "MaintainConnection", 0);' + ); + $this->RegisterTimer( + "TokenRefreshTimer", + 0, + 'IPS_RequestAction(' . $this->InstanceID . ', "RefreshToken", 0);' + ); + + $this->RequireParent(self::WEBSOCKET_MODULE_ID); + } + + public function ApplyChanges() + { + parent::ApplyChanges(); + + $this->SetTimerInterval("MaintainConnectionTimer", 0); + $this->SetTimerInterval("TokenRefreshTimer", 0); + $this->SetBuffer("SignalRReady", "0"); + $this->SetBuffer("ReceiveBuffer", ""); + $this->SetBuffer("Subscriptions", "{}"); + $this->SetBuffer("SubscribedThisConnection", "{}"); + $this->SetBuffer("PendingInvocations", "{}"); + $this->SetValue("SubscriptionCount", 0); + $this->SetConnected(false); + + if (!$this->ReadPropertyBoolean("Active")) { + $this->SetStatus(104); + return; + } + + if ( + trim($this->ReadPropertyString("Username")) === "" || + $this->ReadPropertyString("Password") === "" + ) { + $this->SetStatus(201); + $this->SetLastError("Easee-Benutzername oder Passwort fehlt"); + return; + } + + $this->SetTimerInterval("MaintainConnectionTimer", 10000); + $this->SetTimerInterval("TokenRefreshTimer", 1800000); + + if (!$this->RefreshCredentials(false)) { + $this->SetStatus(202); + return; + } + + $this->ConnectSignalR(); + } + + public function RequestAction($Ident, $Value) + { + switch ($Ident) { + case "MaintainConnection": + $this->MaintainConnection(); + break; + + case "RefreshToken": + if ($this->RefreshCredentials(false)) { + $this->ConnectSignalR(); + } + break; + + case "Reconnect": + $this->SetBuffer("AccessToken", ""); + $this->SetBuffer("RefreshToken", ""); + if ($this->RefreshCredentials(false)) { + $this->ConnectSignalR(); + } + break; + + default: + throw new Exception("Unbekannte Aktion"); + } + } + + public function GetConfigurationForParent() + { + return json_encode([ + "Active" => $this->ReadPropertyBoolean("Active"), + "URL" => $this->GetBuffer("WebSocketURL"), + "VerifyCertificate" => $this->ReadPropertyBoolean("VerifyCertificate"), + "Headers" => "[]", + ]); + } + + public function ReceiveData($JSONString) + { + $packet = json_decode($JSONString, true); + if (!is_array($packet) || !isset($packet["Buffer"])) { + return; + } + + $this->SetBuffer("LastReceive", (string)time()); + $buffer = $this->GetBuffer("ReceiveBuffer") . $packet["Buffer"]; + $frames = explode(self::RECORD_SEPARATOR, $buffer); + $this->SetBuffer("ReceiveBuffer", array_pop($frames)); + + foreach ($frames as $frame) { + if ($frame !== "") { + $this->HandleSignalRFrame($frame); + } + } + } + + public function ForwardData($JSONString) + { + $packet = json_decode($JSONString, true); + if (!is_array($packet) || !isset($packet["Buffer"])) { + return json_encode(["success" => false, "error" => "Ungültiges Datenpaket"]); + } + + $request = json_decode($packet["Buffer"], true); + if (!is_array($request) || !isset($request["action"])) { + return json_encode(["success" => false, "error" => "Ungültige Gateway-Anfrage"]); + } + + $serialNumber = strtoupper(trim((string)($request["serialNumber"] ?? ""))); + + switch ($request["action"]) { + case "Subscribe": + if ($serialNumber === "") { + return json_encode(["success" => false, "error" => "Seriennummer fehlt"]); + } + $this->RegisterSubscription($serialNumber); + return $this->CreateStateResponse($serialNumber); + + case "GetState": + if ($serialNumber === "") { + return json_encode(["success" => false, "error" => "Seriennummer fehlt"]); + } + $this->RegisterSubscription($serialNumber); + return $this->CreateStateResponse($serialNumber); + + case "SetCurrent": + return json_encode( + $this->SetDynamicChargerCurrent( + $serialNumber, + (float)($request["amps"] ?? -1) + ) + ); + + default: + return json_encode(["success" => false, "error" => "Unbekannte Gateway-Aktion"]); + } + } + + private function MaintainConnection() + { + if (!$this->ReadPropertyBoolean("Active")) { + return; + } + + $now = time(); + $lastNegotiation = (int)$this->GetBuffer("LastNegotiation"); + $lastReceive = (int)$this->GetBuffer("LastReceive"); + $ready = $this->GetBuffer("SignalRReady") === "1"; + + if ($ready) { + $this->SendSignalRFrame(["type" => 6]); + + if ($lastReceive > 0 && ($now - $lastReceive) <= 90) { + return; + } + + $this->SetConnected(false); + $this->SetBuffer("SignalRReady", "0"); + } + + if (($now - $lastNegotiation) >= 45) { + $this->ConnectSignalR(); + return; + } + + $this->SendHandshake(); + } + + private function ConnectSignalR() + { + if (!$this->EnsureAccessToken()) { + $this->SetStatus(202); + return false; + } + + $this->SetBuffer("LastNegotiation", (string)time()); + $token = $this->GetBuffer("AccessToken"); + $response = $this->HttpRequest( + "POST", + self::SIGNALR_BASE_URL . "/negotiate?negotiateVersion=1", + "", + $token + ); + + if ($response["httpCode"] === 401 && $this->RefreshCredentials(false)) { + $token = $this->GetBuffer("AccessToken"); + $response = $this->HttpRequest( + "POST", + self::SIGNALR_BASE_URL . "/negotiate?negotiateVersion=1", + "", + $token + ); + } + + if (!$response["success"]) { + $this->SetStatus(203); + $this->SetLastError( + "SignalR-Aushandlung fehlgeschlagen: HTTP " . $response["httpCode"] . + ($response["error"] !== "" ? " / " . $response["error"] : "") + ); + return false; + } + + $data = json_decode($response["body"], true); + $connectionToken = is_array($data) + ? ($data["connectionToken"] ?? $data["connectionId"] ?? "") + : ""; + + if ($connectionToken === "") { + $this->SetStatus(203); + $this->SetLastError("SignalR-Antwort enthält kein Verbindungstoken"); + return false; + } + + $webSocketURL = + "wss://streams.easee.com/hubs/chargers?id=" . rawurlencode($connectionToken) . + "&access_token=" . rawurlencode($token); + + $this->SetBuffer("WebSocketURL", $webSocketURL); + $this->SetBuffer("SignalRReady", "0"); + $this->SetBuffer("ReceiveBuffer", ""); + $this->SetBuffer("SubscribedThisConnection", "{}"); + $this->SetBuffer("PendingInvocations", "{}"); + $this->SetConnected(false); + + if (!$this->ConfigureWebSocketParent($webSocketURL)) { + $this->SetStatus(203); + return false; + } + + $this->SendHandshake(); + return true; + } + + private function ConfigureWebSocketParent(string $url): bool + { + $instance = IPS_GetInstance($this->InstanceID); + $parentID = (int)$instance["ConnectionID"]; + if ($parentID <= 0) { + $this->SetLastError("WebSocket-Client ist nicht verbunden"); + return false; + } + + IPS_SetProperty($parentID, "Active", true); + IPS_SetProperty($parentID, "URL", $url); + IPS_SetProperty($parentID, "VerifyCertificate", $this->ReadPropertyBoolean("VerifyCertificate")); + IPS_SetProperty($parentID, "Headers", "[]"); + IPS_ApplyChanges($parentID); + + return true; + } + + private function SendHandshake() + { + $this->SendRawToWebSocket( + json_encode(["protocol" => "json", "version" => 1]) . self::RECORD_SEPARATOR + ); + } + + private function SendSignalRFrame(array $frame) + { + $this->SendRawToWebSocket(json_encode($frame) . self::RECORD_SEPARATOR); + } + + private function SendRawToWebSocket(string $payload) + { + return $this->SendDataToParent(json_encode([ + "DataID" => self::SIMPLE_TX_DATA_ID, + "Buffer" => $payload, + ])); + } + + private function HandleSignalRFrame(string $frame) + { + if ($frame === "{}") { + $this->SetBuffer("SignalRReady", "1"); + $this->SetStatus(102); + $this->SetLastError(""); + $this->SetConnected(true); + $this->SendAllSubscriptions(); + return; + } + + $message = json_decode($frame, true); + if (!is_array($message)) { + $this->SetLastError("Ungültige SignalR-Nachricht"); + return; + } + + if (isset($message["error"])) { + $this->SetStatus(203); + $this->SetLastError("SignalR-Handshake: " . $message["error"]); + return; + } + + $type = (int)($message["type"] ?? 0); + if ($type === 6) { + return; + } + + if ($type === 7) { + $this->SetBuffer("SignalRReady", "0"); + $this->SetConnected(false); + $this->SetLastError("SignalR-Verbindung wurde beendet"); + return; + } + + $serialNumber = ""; + if ($type === 3 && isset($message["invocationId"])) { + $pending = $this->ReadBufferArray("PendingInvocations"); + $invocationID = (string)$message["invocationId"]; + $serialNumber = (string)($pending[$invocationID] ?? ""); + unset($pending[$invocationID]); + $this->SetBuffer("PendingInvocations", json_encode($pending)); + } + + if (isset($message["arguments"])) { + $this->ExtractObservations($message["arguments"], $serialNumber); + } + if (isset($message["result"])) { + $this->ExtractObservations($message["result"], $serialNumber); + } + } + + private function RegisterSubscription(string $serialNumber) + { + $subscriptions = $this->ReadBufferArray("Subscriptions"); + if (!isset($subscriptions[$serialNumber])) { + $subscriptions[$serialNumber] = true; + $this->SetBuffer("Subscriptions", json_encode($subscriptions)); + $this->SetValue("SubscriptionCount", count($subscriptions)); + } + + if ($this->GetBuffer("SignalRReady") === "1") { + $this->SendSubscription($serialNumber); + } + } + + private function SendAllSubscriptions() + { + foreach (array_keys($this->ReadBufferArray("Subscriptions")) as $serialNumber) { + $this->SendSubscription($serialNumber); + } + } + + private function SendSubscription(string $serialNumber) + { + $sent = $this->ReadBufferArray("SubscribedThisConnection"); + if (isset($sent[$serialNumber])) { + return; + } + + $invocationID = (string)(((int)$this->GetBuffer("InvocationID")) + 1); + $this->SetBuffer("InvocationID", $invocationID); + + $pending = $this->ReadBufferArray("PendingInvocations"); + $pending[$invocationID] = $serialNumber; + $this->SetBuffer("PendingInvocations", json_encode($pending)); + + $this->SendSignalRFrame([ + "type" => 1, + "invocationId" => $invocationID, + "target" => "SubscribeWithCurrentState", + "arguments" => [$serialNumber, true], + ]); + + $sent[$serialNumber] = true; + $this->SetBuffer("SubscribedThisConnection", json_encode($sent)); + } + + private function ExtractObservations($node, string $serialNumber = "", int $depth = 0) + { + if (!is_array($node) || $depth > 12) { + return; + } + + foreach (["serialNumber", "SerialNumber", "chargerId", "ChargerId", "mid", "Mid"] as $key) { + if (isset($node[$key]) && is_scalar($node[$key])) { + $candidate = strtoupper((string)$node[$key]); + if ($this->IsSubscribedSerial($candidate)) { + $serialNumber = $candidate; + break; + } + } + } + + if ($this->IsList($node)) { + foreach ($node as $item) { + if (is_string($item)) { + $candidate = strtoupper($item); + if ($this->IsSubscribedSerial($candidate)) { + $serialNumber = $candidate; + } + } + } + } + + $observationID = null; + foreach (["id", "Id", "observationId", "ObservationId"] as $key) { + if (array_key_exists($key, $node) && is_numeric($node[$key])) { + $observationID = (int)$node[$key]; + break; + } + } + + $valueExists = false; + $value = null; + foreach (["value", "Value"] as $key) { + if (array_key_exists($key, $node)) { + $valueExists = true; + $value = $node[$key]; + break; + } + } + + if ( + $serialNumber !== "" && + $valueExists && + ($observationID === 109 || $observationID === 120) + ) { + $this->PublishObservation($serialNumber, $observationID, $value); + } + + foreach ($node as $item) { + if (is_array($item)) { + $this->ExtractObservations($item, $serialNumber, $depth + 1); + } + } + } + + private function PublishObservation(string $serialNumber, int $observationID, $value) + { + $cache = $this->ReadAttributeArray("ObservationCache"); + if (!isset($cache[$serialNumber]) || !is_array($cache[$serialNumber])) { + $cache[$serialNumber] = []; + } + $cache[$serialNumber][(string)$observationID] = $value; + $cache[$serialNumber]["updated"] = time(); + $this->WriteAttributeString("ObservationCache", json_encode($cache)); + + $this->SendDataToChildren(json_encode([ + "DataID" => self::CHILD_EVENT_DATA_ID, + "Buffer" => json_encode([ + "type" => "Observation", + "serialNumber" => $serialNumber, + "id" => $observationID, + "value" => $value, + "timestamp" => time(), + ]), + ])); + } + + private function CreateStateResponse(string $serialNumber): string + { + $cache = $this->ReadAttributeArray("ObservationCache"); + return json_encode([ + "success" => true, + "connected" => $this->GetBuffer("SignalRReady") === "1", + "state" => $cache[$serialNumber] ?? [], + ]); + } + + private function SetDynamicChargerCurrent(string $serialNumber, float $amps): array + { + if ($serialNumber === "") { + return ["success" => false, "error" => "Seriennummer fehlt"]; + } + if ($amps < 0 || $amps > 32) { + return ["success" => false, "error" => "Strom muss zwischen 0 und 32 A liegen"]; + } + + return $this->AuthorizedApiRequest( + "POST", + "/api/chargers/" . rawurlencode($serialNumber) . + "/commands/set_dynamic_charger_current", + json_encode(["amps" => $amps, "minutes" => 0]) + ); + } + + private function AuthorizedApiRequest(string $method, string $path, string $body): array + { + if (!$this->EnsureAccessToken()) { + return ["success" => false, "error" => "Kein Easee-Access-Token"]; + } + + $response = $this->HttpRequest( + $method, + self::API_BASE_URL . $path, + $body, + $this->GetBuffer("AccessToken") + ); + + if ($response["httpCode"] === 401 && $this->RefreshCredentials(false)) { + $response = $this->HttpRequest( + $method, + self::API_BASE_URL . $path, + $body, + $this->GetBuffer("AccessToken") + ); + } + + if (!$response["success"]) { + $error = $response["error"]; + if ($error === "") { + $error = "Easee-HTTP-Fehler " . $response["httpCode"]; + } + return [ + "success" => false, + "error" => $error, + "httpCode" => $response["httpCode"], + ]; + } + + return [ + "success" => true, + "httpCode" => $response["httpCode"], + "body" => $response["body"], + ]; + } + + public function RefreshCredentials(bool $reconnect = true): bool + { + $tokens = null; + $accessToken = $this->GetBuffer("AccessToken"); + $refreshToken = $this->GetBuffer("RefreshToken"); + + if ($accessToken !== "" && $refreshToken !== "") { + $tokens = $this->RequestTokenPair( + self::API_BASE_URL . "/api/accounts/refresh_token", + [ + "accessToken" => $accessToken, + "refreshToken" => $refreshToken, + ], + $accessToken + ); + } + + if ($tokens === null) { + $tokens = $this->RequestTokenPair( + self::API_BASE_URL . "/api/accounts/login", + [ + "userName" => $this->ReadPropertyString("Username"), + "password" => $this->ReadPropertyString("Password"), + ] + ); + } + + if ($tokens === null || !isset($tokens["accessToken"])) { + $this->SetLastError("Easee-Anmeldung oder Token-Erneuerung fehlgeschlagen"); + $this->SetStatus(202); + return false; + } + + $this->SetBuffer("AccessToken", (string)$tokens["accessToken"]); + if (isset($tokens["refreshToken"])) { + $this->SetBuffer("RefreshToken", (string)$tokens["refreshToken"]); + } + $this->SetBuffer("TokenUpdated", (string)time()); + + if ($reconnect) { + $this->ConnectSignalR(); + } + + return true; + } + + private function EnsureAccessToken(): bool + { + if ($this->GetBuffer("AccessToken") !== "") { + return true; + } + return $this->RefreshCredentials(false); + } + + private function RequestTokenPair(string $url, array $payload, string $bearerToken = "") + { + $response = $this->HttpRequest( + "POST", + $url, + json_encode($payload), + $bearerToken + ); + if (!$response["success"]) { + return null; + } + + $data = json_decode($response["body"], true); + return is_array($data) ? $data : null; + } + + private function HttpRequest( + string $method, + string $url, + string $body = "", + string $bearerToken = "" + ): array { + $headers = [ + "Accept: application/json", + "Content-Type: application/json", + ]; + if ($bearerToken !== "") { + $headers[] = "Authorization: Bearer " . $bearerToken; + } + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_TIMEOUT => 30, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POSTFIELDS => $body, + ]); + + $responseBody = curl_exec($ch); + $curlError = curl_error($ch); + $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return [ + "success" => $responseBody !== false && $curlError === "" && + $httpCode >= 200 && $httpCode < 300, + "body" => $responseBody === false ? "" : $responseBody, + "error" => $curlError, + "httpCode" => $httpCode, + ]; + } + + private function SetConnected(bool $connected) + { + $this->SetValue("Connected", $connected); + $this->SendDataToChildren(json_encode([ + "DataID" => self::CHILD_EVENT_DATA_ID, + "Buffer" => json_encode([ + "type" => "GatewayStatus", + "connected" => $connected, + ]), + ])); + } + + private function SetLastError(string $message) + { + $this->SetValue("LastError", $message); + if ($message !== "") { + $this->SendDebug("EaseeGateway", $message, 0); + } + } + + private function IsSubscribedSerial(string $candidate): bool + { + return isset($this->ReadBufferArray("Subscriptions")[$candidate]); + } + + private function IsList(array $value): bool + { + $index = 0; + foreach ($value as $key => $_) { + if ($key !== $index++) { + return false; + } + } + return true; + } + + private function ReadAttributeArray(string $name): array + { + $value = json_decode($this->ReadAttributeString($name), true); + return is_array($value) ? $value : []; + } + + private function ReadBufferArray(string $name): array + { + $value = json_decode($this->GetBuffer($name), true); + return is_array($value) ? $value : []; + } +} diff --git a/Ladestation_v2/README.md b/Ladestation_v2/README.md index b059e3a..831bf83 100644 --- a/Ladestation_v2/README.md +++ b/Ladestation_v2/README.md @@ -1,67 +1,34 @@ -# Manager_1 -Beschreibung des Moduls. +# Ladestation_v2 -### Inhaltsverzeichnis +Das Modul steuert verschiedene Ladestationstypen. -1. [Funktionsumfang](#1-funktionsumfang) -2. [Voraussetzungen](#2-voraussetzungen) -3. [Software-Installation](#3-software-installation) -4. [Einrichten der Instanzen in IP-Symcon](#4-einrichten-der-instanzen-in-ip-symcon) -5. [Statusvariablen und Profile](#5-statusvariablen-und-profile) -6. [WebFront](#6-webfront) -7. [PHP-Befehlsreferenz](#7-php-befehlsreferenz) +## Easee -### 1. Funktionsumfang +Die Ladestationstypen 5 und 6 verwenden das übergeordnete +`Easee SignalR Gateway`. -* +- Typ 5: Easee mit zusätzlicher eCarUp-Benutzerprüfung für Solarladen +- Typ 6: Easee ohne eCarUp-Benutzerprüfung -### 2. Voraussetzungen +Die Easee-Zugangsdaten werden ausschließlich im Gateway eingetragen. In der +Ladestationsinstanz muss die Easee-Seriennummer hinterlegt sein. -- IP-Symcon ab Version 7.1 +Das Gateway liefert: -### 3. Software-Installation +- Observation 109: Fahrzeug-/Ladestatus +- Observation 120: aktuelle Gesamtleistung in kW -* Über den Module Store das 'Manager_1'-Modul installieren. -* Alternativ über das Module Control folgende URL hinzufügen +Stromvorgaben von 0 bis 32 A werden ebenfalls über das Gateway gesendet. -### 4. Einrichten der Instanzen in IP-Symcon +## Andere Ladestationstypen - Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. - - Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen) +Die Typen 1 bis 4 arbeiten unverändert und benötigen kein Easee-Gateway. -__Konfigurationsseite__: +## Einrichtung -Name | Beschreibung --------- | ------------------ - | - | +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. Die Ladestationsinstanz mit dem gewünschten Gateway verbinden. -### 5. Statusvariablen und Profile - -Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen. - -#### Statusvariablen - -Name | Typ | Beschreibung ------- | ------- | ------------ - | | - | | - -#### Profile - -Name | Typ ------- | ------- - | - | - -### 6. WebFront - -Die Funktionalität, die das Modul im WebFront bietet. - -### 7. PHP-Befehlsreferenz - -`boolean GEF_BeispielFunktion(integer $InstanzID);` -Erklärung der Funktion. - -Beispiel: -`GEF_BeispielFunktion(12345);` \ No newline at end of file +Mehrere Easee-Ladestationen desselben Kontos verwenden gemeinsam ein Gateway. diff --git a/Ladestation_v2/form.json b/Ladestation_v2/form.json index be54d2e..0e6274e 100644 --- a/Ladestation_v2/form.json +++ b/Ladestation_v2/form.json @@ -10,7 +10,7 @@ "value": 1 }, { - "caption": "Go-E Germini / Germini Flex", + "caption": "Go-E Gemini / Gemini Flex", "value": 2 }, { @@ -34,7 +34,7 @@ { "type": "NumberSpinner", "name": "IdleCounterMax", - "caption": "Zyklen zwischen zwei Leistungsänderungen (Multipliziert sich mit Interval)", + "caption": "Zyklen zwischen zwei Leistungsänderungen (multipliziert mit Intervall)", "suffix": "" }, { @@ -52,7 +52,7 @@ { "type": "NumberSpinner", "name": "Zeit_Zwischen_Zustandswechseln", - "caption": "(Veraltet, auf 0 lassen, wird in nächster Version entfehrnt) Mindestlaufzeit des Verbrauchers bei Lastschaltung ", + "caption": "(Veraltet, auf 0 lassen) Mindestlaufzeit des Verbrauchers bei Lastschaltung", "suffix": "" }, { @@ -71,42 +71,36 @@ "type": "ValidationTextBox", "name": "IP_Adresse", "caption": "IP-Adresse Ladestation" - }, { "type": "ValidationTextBox", - "caption": "Geräte-ID Smart-Me / ECarUp / Easee", - "name": "ID" - + "name": "ID", + "caption": "Geräte-ID Smart-Me / eCarUp / Easee" }, { "type": "ValidationTextBox", - "caption": "Seriennummer Smart-Me / ECarUp / Easee", - "name": "Seriennummer" - + "name": "Seriennummer", + "caption": "Seriennummer Smart-Me / eCarUp / Easee" }, { "type": "ValidationTextBox", - "caption": "Username / Benutzernahme Solarladen", - "name": "Username" - - }, - { "type": "PasswordTextBox", - "caption": "Password -> Bei Anwendung mit Pico-Stationen", - "name": "Password" + "name": "Username", + "caption": "Benutzername Smart-Me / Benutzerkennung Solarladen" }, { - "type": "SelectVariable", - "name": "Token_Easee", - "caption": "Variable API-Token für Easee", - "suffix": "" + "type": "PasswordTextBox", + "name": "Password", + "caption": "Passwort Smart-Me" }, { "type": "SelectVariable", "name": "Token_ECarUp", - "caption": "Variable API-Token für ECarUp", + "caption": "Variable API-Token für eCarUp", "suffix": "" + }, + { + "type": "Label", + "caption": "Die Typen 5 und 6 verwenden das verbundene Easee SignalR Gateway. Easee-Zugangsdaten werden im Gateway konfiguriert." } - ] -} \ No newline at end of file +} diff --git a/Ladestation_v2/module.json b/Ladestation_v2/module.json index b1e81f3..0dba1f7 100644 --- a/Ladestation_v2/module.json +++ b/Ladestation_v2/module.json @@ -4,9 +4,13 @@ "type": 3, "vendor": "Belevo AG", "aliases": [], - "parentRequirements": [], + "parentRequirements": [ + "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}" + ], "childRequirements": [], - "implemented": [], + "implemented": [ + "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}" + ], "prefix": "GEF", "url": "" -} \ No newline at end of file +} diff --git a/Ladestation_v2/module.php b/Ladestation_v2/module.php index becf46d..f02fd36 100644 --- a/Ladestation_v2/module.php +++ b/Ladestation_v2/module.php @@ -2,6 +2,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}"; + public function Create() { @@ -19,7 +23,6 @@ class Ladestation_v2 extends IPSModule $this->RegisterPropertyInteger("Zeit_Zwischen_Zustandswechseln", 1); $this->RegisterPropertyInteger("Ein_Zeit", 0); // Recheninterval $this->RegisterPropertyInteger("Aus_Zeit", 0); // Recheninterval - $this->RegisterPropertyInteger("Token_Easee", 0); // Recheninterval $this->RegisterPropertyInteger("Token_ECarUp", 0); // Recheninterval @@ -87,8 +90,14 @@ class Ladestation_v2 extends IPSModule $this->RegisterVariableInteger("Leistung_Delta", "Leistung_Delta", "", 0); IPS_SetHidden($this->GetIDForIdent("Leistung_Delta"), true); - $this->RegisterVariableString("Token_Intern", "Internes Token Easee"); - IPS_SetHidden($this->GetIDForIdent("Token_Intern"), true); + $this->RegisterVariableBoolean( + "Easee_Gateway_Connected", + "Easee Gateway verbunden", + "~Switch", + false + ); + IPS_SetHidden($this->GetIDForIdent("Easee_Gateway_Connected"), true); + $this->SetBuffer("EaseeGatewayState", "{}"); // Hilfsvariabeln für Idle zustand $this->RegisterPropertyInteger("IdleCounterMax", 2); @@ -100,8 +109,6 @@ class Ladestation_v2 extends IPSModule $this->SetValue("Idle", true); $this->RegisterTimer("Timer_Do_UserCalc_EVC",$this->ReadPropertyInteger("Interval")*1000,"IPS_RequestAction(" .$this->InstanceID .', "Do_UserCalc", "");'); - $this->RegisterTimer("Timer_Refresh_Token",0,"IPS_RequestAction(" .$this->InstanceID .', "Refresh_Token", "");'); - $this->RegisterVariableInteger("Mindestaldestrom", "Mindestaldestrom", "", 0); $this->EnableAction("Mindestaldestrom"); @@ -111,15 +118,30 @@ class Ladestation_v2 extends IPSModule { parent::ApplyChanges(); $this->SetTimerInterval("Timer_Do_UserCalc_EVC",$this->ReadPropertyInteger("Interval")*1000); + $stationType = $this->ReadPropertyInteger("Ladestation"); + $usesEaseeGateway = $stationType === 5 || $stationType === 6; + IPS_SetHidden( + $this->GetIDForIdent("Easee_Gateway_Connected"), + !$usesEaseeGateway + ); - // erstelle einen Timer der das Request token aktualisiert wenn die station dies braucht. - if($this->ReadPropertyInteger("Ladestation")==6){ - - $this->Refresh_Token(); - $this->SetTimerInterval("Timer_Refresh_Token",1800000); + $instance = IPS_GetInstance($this->InstanceID); + $parentID = (int)$instance["ConnectionID"]; + if ($usesEaseeGateway) { + if ($parentID <= 0) { + $this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID); + } + $this->SubscribeToEaseeGateway(); + } elseif ($parentID > 0) { + $parent = IPS_GetInstance($parentID); + if ($parent["ModuleInfo"]["ModuleID"] === self::EASEE_GATEWAY_MODULE_ID) { + IPS_DisconnectInstance($this->InstanceID); + } + $this->SetValue("Easee_Gateway_Connected", false); } + // erstelle einen Timer der das Request token aktualisiert wenn die station dies braucht. // Zusätzliche Anpassungen nach Bedarf } @@ -175,10 +197,6 @@ class Ladestation_v2 extends IPSModule $this->ResetNullTimer(); break; - case "Refresh_Token": - $this->Refresh_Token(); - break; - case "Mindestaldestrom": $this->SetValue("Mindestaldestrom", (int)$Value); break; @@ -188,6 +206,191 @@ class Ladestation_v2 extends IPSModule } } + public function ReceiveData($JSONString) + { + $packet = json_decode($JSONString, true); + if (!is_array($packet) || !isset($packet["Buffer"])) { + return; + } + + $message = json_decode($packet["Buffer"], true); + if (!is_array($message) || !isset($message["type"])) { + return; + } + + if ($message["type"] === "GatewayStatus") { + $this->SetValue( + "Easee_Gateway_Connected", + (bool)($message["connected"] ?? false) + ); + return; + } + + if ( + $message["type"] !== "Observation" || + strtoupper((string)($message["serialNumber"] ?? "")) !== + strtoupper($this->ReadPropertyString("Seriennummer")) + ) { + return; + } + + $this->StoreEaseeObservation( + (int)($message["id"] ?? 0), + $message["value"] ?? null, + (int)($message["timestamp"] ?? time()) + ); + } + + private function SubscribeToEaseeGateway() + { + $serialNumber = strtoupper(trim($this->ReadPropertyString("Seriennummer"))); + if ($serialNumber === "") { + return; + } + + $response = $this->SendEaseeGatewayRequest([ + "action" => "Subscribe", + "serialNumber" => $serialNumber, + ]); + $this->ApplyEaseeGatewayResponse($response); + } + + private function GetEaseeStateFromGateway() + { + $serialNumber = strtoupper(trim($this->ReadPropertyString("Seriennummer"))); + if ($serialNumber === "") { + return; + } + + $response = $this->SendEaseeGatewayRequest([ + "action" => "GetState", + "serialNumber" => $serialNumber, + ]); + $this->ApplyEaseeGatewayResponse($response); + } + + private function SendEaseeGatewayRequest(array $request) + { + $instance = IPS_GetInstance($this->InstanceID); + if ((int)$instance["ConnectionID"] <= 0) { + $this->SetValue("Easee_Gateway_Connected", false); + return null; + } + + $response = $this->SendDataToParent(json_encode([ + "DataID" => self::EASEE_GATEWAY_REQUEST_ID, + "Buffer" => json_encode($request), + ])); + + if (!is_string($response) || $response === "") { + return null; + } + + $decoded = json_decode($response, true); + return is_array($decoded) ? $decoded : null; + } + + private function ApplyEaseeGatewayResponse($response) + { + if (!is_array($response)) { + $this->SetValue("Easee_Gateway_Connected", false); + return; + } + + if (array_key_exists("connected", $response)) { + $this->SetValue( + "Easee_Gateway_Connected", + (bool)$response["connected"] + ); + } + + if (!isset($response["state"]) || !is_array($response["state"])) { + return; + } + + $updated = (int)($response["state"]["updated"] ?? time()); + foreach ([109, 120] as $observationID) { + $key = (string)$observationID; + if (array_key_exists($key, $response["state"])) { + $this->StoreEaseeObservation( + $observationID, + $response["state"][$key], + $updated + ); + } + } + } + + private function StoreEaseeObservation(int $observationID, $value, int $timestamp) + { + if ($observationID !== 109 && $observationID !== 120) { + return; + } + + $state = json_decode($this->GetBuffer("EaseeGatewayState"), true); + if (!is_array($state)) { + $state = []; + } + $state[(string)$observationID] = $value; + $state["updated"] = $timestamp; + $this->SetBuffer("EaseeGatewayState", json_encode($state)); + + if ($observationID === 109) { + $this->SetValue("Fahrzeugstatus", (int)$value); + } elseif ($observationID === 120) { + $this->SetValue("Ladeleistung_Effektiv", round((float)$value * 1000)); + } + } + + private function GetEaseeCarStatusFromGateway(bool $currentState): bool + { + $this->GetEaseeStateFromGateway(); + $state = json_decode($this->GetBuffer("EaseeGatewayState"), true); + if (!is_array($state) || !array_key_exists("109", $state)) { + return $currentState; + } + + $chargerOpMode = (int)$state["109"]; + if ( + $chargerOpMode != 1 && + ($chargerOpMode != 6 || $this->GetValue("Car_detected") == true) && + $chargerOpMode != 7 + ) { + $currentState = true; + } else { + $currentState = false; + } + + if (array_key_exists("120", $state)) { + $this->SetValue( + "Ladeleistung_Effektiv", + round((float)$state["120"] * 1000) + ); + } + $this->SetValue("Fahrzeugstatus", $chargerOpMode); + + return $currentState; + } + + private function SendEaseeCurrentToGateway(float $value) + { + $response = $this->SendEaseeGatewayRequest([ + "action" => "SetCurrent", + "serialNumber" => strtoupper(trim($this->ReadPropertyString("Seriennummer"))), + "amps" => $value, + ]); + + if (!is_array($response) || !($response["success"] ?? false)) { + $error = is_array($response) + ? (string)($response["error"] ?? "Unbekannter Gateway-Fehler") + : "Easee Gateway nicht erreichbar"; + IPS_LogMessage("Ladestation_v2", $error); + return $error; + } + + return $response["body"] ?? null; + } + public function Detect_Car(int $carType) { @@ -322,10 +525,10 @@ class Ladestation_v2 extends IPSModule public function Get_Car_Status(int $carType) { - $ch = curl_init(); $car_on_station = $this->GetValue("Car_detected"); switch ($carType) { case 1: + $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://" . $this->ReadPropertyString("IP_Adresse") . "/mqtt?payload="); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $response = curl_exec($ch); @@ -351,6 +554,7 @@ class Ladestation_v2 extends IPSModule break; case 2: + $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://" . $this->ReadPropertyString("IP_Adresse") . "/api/status"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $response = curl_exec($ch); @@ -376,6 +580,7 @@ class Ladestation_v2 extends IPSModule break; case 3: + $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "https://api.smart-me.com/pico/charging/" . $this->ReadPropertyString("ID")); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($ch, CURLOPT_USERPWD, $this->ReadPropertyString("Username") . ":" . $this->ReadPropertyString("Password")); @@ -438,83 +643,12 @@ class Ladestation_v2 extends IPSModule } } - //Aktueller Zustand Ladestation abfragen (Leistung, auto eingesteckt?) - $ch2 = curl_init(); - - $url2 = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/state"; - curl_setopt_array($ch2, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "GET", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".GetValue($this->ReadPropertyInteger("Token_Easee"))."", - "content-type: application/*+json" - ], - ]); - - curl_setopt($ch2, CURLOPT_URL, $url2); - curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); - $response_easee = curl_exec($ch2); - curl_close($ch2); - - $easee_data = json_decode($response_easee, true); - - - if (json_last_error() === JSON_ERROR_NONE && isset($easee_data["chargerOpMode"])) { - - if ($easee_data["chargerOpMode"] != 1 && (($easee_data["chargerOpMode"] != 6)||($this->GetValue("Car_detected")==true)) && $easee_data["chargerOpMode"] != 7) { - $car_on_station = true; - } - else{ - $car_on_station = false; - } - $this->SetValue("Ladeleistung_Effektiv", round($easee_data["totalPower"]*1000)); - $this->SetValue("Fahrzeugstatus", $easee_data["chargerOpMode"]); - } + $car_on_station = $this->GetEaseeCarStatusFromGateway($car_on_station); break; - case 6: - - - - //Aktueller Zustand Ladestation abfragen (Leistung, auto eingesteckt?) - $ch2 = curl_init(); - - $url2 = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/state"; - curl_setopt_array($ch2, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "GET", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".$this->GetBuffer("Token_Intern")."", - "content-type: application/*+json" - ], - ]); - - curl_setopt($ch2, CURLOPT_URL, $url2); - curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true); - $response_easee = curl_exec($ch2); - curl_close($ch2); - - $easee_data = json_decode($response_easee, true); - - - if (json_last_error() === JSON_ERROR_NONE && isset($easee_data["chargerOpMode"])) { - - if ($easee_data["chargerOpMode"] != 1 && (($easee_data["chargerOpMode"] != 6)||($this->GetValue("Car_detected")==true)) && $easee_data["chargerOpMode"] != 7) { - $car_on_station = true; - } - else{ - $car_on_station = false; - } - $this->SetValue("Ladeleistung_Effektiv", round($easee_data["totalPower"]*1000)); - $this->SetValue("Fahrzeugstatus", $easee_data["chargerOpMode"]); - } + case 6: + $car_on_station = $this->GetEaseeCarStatusFromGateway($car_on_station); break; @@ -706,11 +840,11 @@ class Ladestation_v2 extends IPSModule } 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 ($is_1_ph) { - $powerSteps = [$this->GetValue("Mindestaldestrom") * 230]; - } else { - $powerSteps = [$this->GetValue("Mindestaldestrom") * 400 * 1.71]; - } + 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")); @@ -758,6 +892,16 @@ class Ladestation_v2 extends IPSModule { $baseUrl = ""; $stationType = $this->ReadPropertyInteger("Ladestation"); + + if ($stationType === 5 || $stationType === 6) { + if ( + $stationType === 5 && + $this->ReadPropertyString("Username") != $this->GetValue("Letzer_User") + ) { + return; + } + return $this->SendEaseeCurrentToGateway($value); + } switch ($stationType) { case 1: @@ -772,12 +916,6 @@ class Ladestation_v2 extends IPSModule case 4: // Nichts zu tun für Dummy station return; - case 5: - // Keine base Url nötig - break; - case 6: - // Keine base Url nötig - break; } $ch = curl_init(); @@ -801,42 +939,6 @@ class Ladestation_v2 extends IPSModule case 4: // Nichts zu tun für Dummy station return; - case 5: - if($this->ReadPropertyString("Username") != $this->GetValue("Letzer_User")){ - return; - } - $url = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/commands/set_dynamic_charger_current"; - curl_setopt_array($ch, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "POST", - CURLOPT_POSTFIELDS => "{\"amps\":". $value .",\"minutes\":0}", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".GetValue($this->ReadPropertyInteger("Token_Easee"))."", - "content-type: application/*+json" - ], - ]); - break; - - case 6: - - $url = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/commands/set_dynamic_charger_current"; - curl_setopt_array($ch, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "POST", - CURLOPT_POSTFIELDS => "{\"amps\":". $value .",\"minutes\":0}", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".$this->GetBuffer("Token_Intern")."", - "content-type: application/*+json" - ], - ]); - break; - default: return "Invalid station type."; } @@ -864,42 +966,6 @@ class Ladestation_v2 extends IPSModule case 4: // Nichts zu tun für Dummy station return; - case 5: - if($this->ReadPropertyString("Username") != $this->GetValue("Letzer_User")){ - return; - } - $url2 = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/commands/set_dynamic_charger_current"; - curl_setopt_array($ch, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "POST", - CURLOPT_POSTFIELDS => "{\"amps\":". $value .",\"minutes\":0}", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".GetValue($this->ReadPropertyInteger("Token_Easee"))."", - "content-type: application/*+json" - ], - ]); - break; - - case 6: - - $url2 = "https://api.easee.com/api/chargers/".$this->ReadPropertyString("Seriennummer")."/commands/set_dynamic_charger_current"; - curl_setopt_array($ch, [ - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => "POST", - CURLOPT_POSTFIELDS => "{\"amps\":". $value .",\"minutes\":0}", - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer ".$this->GetBuffer("Token_Intern")."", - "content-type: application/*+json" - ], - ]); - break; - default: return "Invalid station type."; } @@ -927,43 +993,6 @@ class Ladestation_v2 extends IPSModule } } - public function Refresh_Token(){ - - $payload = json_encode([ - 'userName' => $this->ReadPropertyString("Username"), - 'password' => $this->ReadPropertyString("Password"), - ]); - - // cURL-Handle initialisieren - $ch = curl_init('https://api.easee.com/api/accounts/login'); - - // cURL-Optionen setzen - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, // Antwort als String zurückgeben - CURLOPT_POST => true, // POST-Methode verwenden - CURLOPT_HTTPHEADER => [ - 'Accept: application/json', - 'Content-Type: application/json', - ], - CURLOPT_POSTFIELDS => $payload, // JSON-Body übergeben - ]); - - // Anfrage ausführen und Antwort speichern - $response = curl_exec($ch); - - // Auf Fehler prüfen - if ($response === false) { - echo 'cURL-Fehler: ' . curl_error($ch); - curl_close($ch); - exit; - } - - // cURL-Handle schließen - curl_close($ch); - $this->SetBuffer("Token_Intern", (json_decode($response, true)['accessToken'])); - - } - public function CheckIdle($power) { $lastpower = GetValue($this->GetIDForIdent("Aktuelle_Leistung"));