no message
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
<?php
|
||||
|
||||
class EaseeGateway extends IPSModule
|
||||
{
|
||||
private const WEBSOCKET_MODULE_ID = "{D68FD31F-0E90-7019-F16C-1949BD3079EF}";
|
||||
private const SIMPLE_RX_DATA_ID = "{018EF6B5-AB94-40C6-AA53-46943E824ACF}";
|
||||
private const SIMPLE_TX_DATA_ID = "{79827379-F36E-4ADA-8A95-5F8D1DC92FA9}";
|
||||
private const CHILD_REQUEST_DATA_ID = "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}";
|
||||
private const CHILD_EVENT_DATA_ID = "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}";
|
||||
private const SIGNALR_BASE_URL = "https://streams.easee.com/hubs/chargers";
|
||||
private const API_BASE_URL = "https://api.easee.com";
|
||||
private const RECORD_SEPARATOR = "\x1e";
|
||||
|
||||
public function Create()
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->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 : [];
|
||||
}
|
||||
}
|
||||
+21
-54
@@ -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);`
|
||||
Mehrere Easee-Ladestationen desselben Kontos verwenden gemeinsam ein Gateway.
|
||||
|
||||
+18
-24
@@ -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."
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||
+238
-209
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user