Author SHA1 Message Date
mh 21650e07cf no message 2026-09-17 08:04:55 +02:00
mh 17946b3a02 no message 2026-09-04 10:18:04 +02:00
dh 20324509b1 no message 2026-08-25 17:07:27 +02:00
dh 6b8db1acec no message 2026-08-25 16:59:15 +02:00
dh 6edd688c1f no message 2026-08-25 16:42:17 +02:00
dh ab9f65b5b1 no message 2026-08-25 16:06:34 +02:00
dh 1526b48ef2 no message 2026-08-25 15:36:10 +02:00
dh 21d14d52ed no message 2026-08-25 14:43:45 +02:00
dh 7c1b992729 no message 2026-08-25 13:32:32 +02:00
dh 4491e244fb no message 2026-08-25 11:59:30 +02:00
dh c46bcd0488 no message 2026-08-25 11:39:16 +02:00
dh b4fade6fe7 no message 2026-08-25 10:27:10 +02:00
dh f4ee0ea78a no message 2026-08-25 09:33:47 +02:00
mh 125cc14c94 no message 2026-08-19 15:36:56 +02:00
mh 7959e7e1ae no message 2026-08-19 15:14:00 +02:00
mh 3d4c30ae02 no message 2026-08-19 14:40:04 +02:00
mh d254eaa60f no message 2026-08-19 14:07:43 +02:00
mh 6dd38afdc2 no message 2026-08-19 10:07:01 +02:00
mh d19fe26299 no message 2026-08-18 07:37:58 +02:00
dh 9da468f89f no message 2026-08-06 16:09:06 +02:00
dh c9ca9661cf no message 2026-08-06 16:00:49 +02:00
dh a87461eefa no message 2026-08-06 15:50:37 +02:00
dh a9d3712ef9 no message 2026-08-06 15:41:28 +02:00
dh 2ca9503c46 no message 2026-08-06 15:14:16 +02:00
dh 4d136315c0 no message 2026-08-06 14:53:15 +02:00
dh db5d53820b no message 2026-08-06 11:50:34 +02:00
dh 6763aec78b no message 2026-08-06 11:30:14 +02:00
dh 87ebda1e4f no message 2026-08-06 11:10:38 +02:00
dh 97ce906c91 no message 2026-08-06 10:59:08 +02:00
dh d37fa96a31 no message 2026-08-06 10:42:23 +02:00
dh d8d3e2c998 no message 2026-08-06 10:37:22 +02:00
dh 43623376e4 no message 2026-08-06 10:18:42 +02:00
dh 4e3c6c1b13 no message 2026-08-06 09:46:56 +02:00
dh 69da32646a no message 2026-08-06 09:43:32 +02:00
dh 875a961afb no message 2026-08-06 09:27:07 +02:00
dh 89f348123f no message 2026-08-06 08:52:53 +02:00
dh 6354fac434 no message 2026-08-06 08:49:17 +02:00
dh 9200e2bc51 no message 2026-08-06 08:35:38 +02:00
dh 650ea80e2b no message 2026-08-06 08:27:18 +02:00
mh 313c42d925 no message 2026-07-28 09:22:21 +02:00
mh f3393a84dc no message 2026-07-28 09:19:00 +02:00
mh 77712ee4fd no message 2026-07-28 09:06:59 +02:00
mh 6ce598542f no message 2026-07-28 09:04:06 +02:00
mh fefd1fe2f6 no message 2026-07-28 08:59:50 +02:00
mh a70aad8d45 no message 2026-07-28 08:55:25 +02:00
mh ec42f28e50 no message 2026-07-22 08:26:06 +02:00
mh a459f66847 no message 2026-07-20 15:15:57 +02:00
mh dc38b89bf6 no message 2026-07-20 14:13:02 +02:00
19 changed files with 2189 additions and 754 deletions
+91 -1
View File
@@ -593,11 +593,25 @@ class Bat_EV_SDL_V4 extends IPSModule
$globalSDLpct = ($sdlTotal > 0.0) ? max(0.0, min(100.0, $virtSDL / $sdlTotal * 100.0)) : 0.0; $globalSDLpct = ($sdlTotal > 0.0) ? max(0.0, min(100.0, $virtSDL / $sdlTotal * 100.0)) : 0.0;
$globalEVpct = ($evTotal > 0.0) ? max(0.0, min(100.0, $virtEV / $evTotal * 100.0)) : 0.0; $globalEVpct = ($evTotal > 0.0) ? max(0.0, min(100.0, $virtEV / $evTotal * 100.0)) : 0.0;
/*
foreach ($batteries as &$bat) { foreach ($batteries as &$bat) {
// Mit globalem virtuellem SoC sortieren. Bei Bedarf später pro Batterie virtualisieren. // Mit globalem virtuellem SoC sortieren. Bei Bedarf später pro Batterie virtualisieren.
$bat["SDL_SOC"] = $globalSDLpct; $bat["SDL_SOC"] = $globalSDLpct;
$bat["EV_SOC"] = $globalEVpct; $bat["EV_SOC"] = $globalEVpct;
} }
unset($bat);*/
//SoC nach priosierung
foreach ($batteries as &$bat) {
// Reale Batterie-SoC als Sortierwert verwenden.
// Laden: Batterie mit tieferem SoC zuerst.
// Entladen: Batterie mit höherem SoC zuerst.
$realSocPct = (float)($bat["real_SOC_pct"] ?? 0.0);
$bat["SDL_SOC"] = $realSocPct;
$bat["EV_SOC"] = $realSocPct;
}
unset($bat); unset($bat);
$distributePower = function (float $targetPower, string $mode) use ($batteries): array { $distributePower = function (float $targetPower, string $mode) use ($batteries): array {
@@ -694,6 +708,82 @@ class Bat_EV_SDL_V4 extends IPSModule
* Batterie liefert real nur 3600 W. * Batterie liefert real nur 3600 W.
* Dann wird EV = 2700 W, SDL = 900 W angenommen. * Dann wird EV = 2700 W, SDL = 900 W angenommen.
*/ */
private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void
{
$totalPowerIst = $this->GetTotalBatteryPowerIstW();
$eps = 0.01;
$sumSoll = $pEvW + $pSdlW;
$sumAbsSoll = abs($pEvW) + abs($pSdlW);
/*
* Die Abweichung zwischen realer Batterieleistung und
* Netto-Soll proportional auf EV und SDL verteilen.
*
* Funktioniert auch bei:
* EV = +42000 W
* SDL = -40000 W
*/
if ($sumAbsSoll > $eps) {
$abweichungW = $totalPowerIst - $sumSoll;
$evAnteil = abs($pEvW) / $sumAbsSoll;
$sdlAnteil = abs($pSdlW) / $sumAbsSoll;
$rawEV = $pEvW + ($abweichungW * $evAnteil);
$rawSDL = $pSdlW + ($abweichungW * $sdlAnteil);
} else {
$rawEV = (float)$this->GetBufferSafe("CUR_EV_VAL");
$rawSDL = (float)$this->GetBufferSafe("CUR_SDL_VAL");
}
$filterAktiv = $this->ReadPropertyBoolean("FilterAktiv");
if ($filterAktiv) {
$tolPct = max(
0.0,
(float)$this->ReadPropertyFloat("FilterTolerancePct")
) / 100.0;
$needHits = max(
1,
(int)$this->ReadPropertyInteger("FilterHits")
);
$fEV = $this->FilterCurrent(
"EV",
$rawEV,
$pEvW,
abs($pEvW) * $tolPct,
$needHits
);
$fSDL = $this->FilterCurrent(
"SDL",
$rawSDL,
$pSdlW,
abs($pSdlW) * $tolPct,
$needHits
);
} else {
$fEV = $rawEV;
$fSDL = $rawSDL;
$this->ClearCurrentFilterBuffers();
}
$this->SetIdentValue(
"Aktuelle_Leistung_EV",
(float)$fEV
);
$this->SetIdentValue(
"Aktuelle_Leistung_SDL",
(float)$fSDL
);
}
/*
private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void private function UpdateActualPowerSplit(float $pEvW, float $pSdlW): void
{ {
$totalPowerIst = $this->GetTotalBatteryPowerIstW(); $totalPowerIst = $this->GetTotalBatteryPowerIstW();
@@ -726,7 +816,7 @@ class Bat_EV_SDL_V4 extends IPSModule
$this->SetIdentValue("Aktuelle_Leistung_EV", (float)$fEV); $this->SetIdentValue("Aktuelle_Leistung_EV", (float)$fEV);
$this->SetIdentValue("Aktuelle_Leistung_SDL", (float)$fSDL); $this->SetIdentValue("Aktuelle_Leistung_SDL", (float)$fSDL);
} }*/
/** /**
* Filtert die rückgerechnete Istleistung. * Filtert die rückgerechnete Istleistung.
* *
+98 -25
View File
@@ -1,5 +1,6 @@
# Manager_1 # CC100_HW
Beschreibung des Moduls.
Hardwareanbindung für die digitalen Ein- und Ausgänge sowie die beiden PT-Temperatursensoren einer CC100-Steuerung.
### Inhaltsverzeichnis ### Inhaltsverzeichnis
@@ -13,55 +14,127 @@ Beschreibung des Moduls.
### 1. Funktionsumfang ### 1. Funktionsumfang
* - Ansteuerung von vier digitalen Ausgängen
- Auswertung von acht digitalen Eingängen
- Auswertung von zwei PT-Temperatursensoren
- Automatisches Einlesen der Temperaturen alle 30 Sekunden
- Automatisches Einlesen der digitalen Eingänge alle 2 Sekunden
- Schreiben der Ausgangszustände in die CC100-Systemdatei
- Umrechnung der digitalen Eingangszahl in acht einzelne Schaltzustände
- Umrechnung der PT-Rohwerte in Temperaturwerte
### 2. Voraussetzungen ### 2. Voraussetzungen
- IP-Symcon ab Version 7.1 - IP-Symcon ab Version 7.1
- Linux-basiertes CC100-System
- Zugriff des IP-Symcon-Dienstes auf die benötigten Systemdateien
- Schreibberechtigung für die Datei der digitalen Ausgänge
- Vorhandene CC100-Kernel- und IIO-Treiber
Folgende Systemdateien werden verwendet:
```text
/sys/kernel/dout_drv/DOUT_DATA
/sys/devices/platform/soc/44009000.spi/spi_master/spi0/spi0.0/din
/sys/bus/iio/devices/iio:device2/in_voltage13_raw
/sys/bus/iio/devices/iio:device2/in_voltage1_raw
```
Das Modul ist wegen dieser fest eingetragenen Systempfade nur für die entsprechende CC100-Hardware vorgesehen.
### 3. Software-Installation ### 3. Software-Installation
* Über den Module Store das 'Manager_1'-Modul installieren. - Das Verzeichnis `CC100_HW` in die verwendete IP-Symcon-Modulbibliothek kopieren.
* Alternativ über das Module Control folgende URL hinzufügen - Alternativ die zugehörige Modulbibliothek über die Modulverwaltung installieren.
- Anschließend die Modulbibliothek neu laden.
### 4. Einrichten der Instanzen in IP-Symcon ### 4. Einrichten der Instanzen in IP-Symcon
Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. Unter `Instanz hinzufügen` kann das Modul `CC100_HW` 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)
__Konfigurationsseite__: Weitere Informationen zum Hinzufügen von Instanzen befinden sich in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen).
**Konfigurationsseite**:
Name | Beschreibung Name | Beschreibung
-------- | ------------------ --- | ---
| FilePath | Dateipfad zum Schreiben der digitalen Ausgangszustände
| Standardwert | `/sys/kernel/dout_drv/DOUT_DATA`
Im aktuellen Modulverzeichnis ist keine `form.json` vorhanden. Die Eigenschaft `FilePath` ist deshalb nicht über eine normale Konfigurationsseite auswählbar und verwendet standardmäßig den im Modul hinterlegten Pfad.
### 5. Statusvariablen und Profile ### 5. Statusvariablen und Profile
Die Statusvariablen/Kategorien werden automatisch angelegt. Das Löschen einzelner kann zu Fehlfunktionen führen. Die Statusvariablen werden beim Erstellen der Instanz automatisch angelegt. Das Löschen einzelner Variablen kann zu Fehlfunktionen führen.
#### Statusvariablen #### Statusvariablen
Name | Typ | Beschreibung Name | Ident | Typ | Beschreibung
------ | ------- | ------------ --- | --- | --- | ---
| | DO 1 | Bit1 | Boolean | Digitaler Ausgang 1
| | DO 2 | Bit2 | Boolean | Digitaler Ausgang 2
DO 3 | Bit3 | Boolean | Digitaler Ausgang 3
DO 4 | Bit4 | Boolean | Digitaler Ausgang 4
DI1 | DI1 | Boolean | Digitaler Eingang 1
DI2 | DI2 | Boolean | Digitaler Eingang 2
DI3 | DI3 | Boolean | Digitaler Eingang 3
DI4 | DI4 | Boolean | Digitaler Eingang 4
DI5 | DI5 | Boolean | Digitaler Eingang 5
DI6 | DI6 | Boolean | Digitaler Eingang 6
DI7 | DI7 | Boolean | Digitaler Eingang 7
DI8 | DI8 | Boolean | Digitaler Eingang 8
PT1 Temperatur | PT1 | Float | Berechnete Temperatur des PT1-Eingangs
PT2 Temperatur | PT2 | Float | Berechnete Temperatur des PT2-Eingangs
#### Profile #### Profile
Name | Typ Name | Typ | Verwendung
------ | ------- --- | --- | ---
| ~Switch | Boolean | Darstellung der digitalen Ausgänge als Schalter
| ~Temperature | Float | Darstellung der PT-Werte als Temperatur
Es werden keine eigenen Variablenprofile erstellt.
### 6. WebFront ### 6. WebFront
Die Funktionalität, die das Modul im WebFront bietet. Im WebFront werden folgende Werte dargestellt:
- Vier digitale Ausgänge
- Acht digitale Eingänge
- Zwei Temperaturwerte
Die digitalen Eingänge werden alle 2 Sekunden aus der CC100-Systemdatei gelesen.
Die PT-Rohwerte werden alle 30 Sekunden eingelesen und anhand der im Modul hinterlegten Skalierungsbereiche in Temperaturwerte umgerechnet.
#### Einschränkung der digitalen Ausgänge
Die vier Ausgangsvariablen sind als bedienbare Schalter registriert. Die aktuelle `RequestAction()`-Implementierung verarbeitet die Identifikatoren `Bit1` bis `Bit4` jedoch nicht.
Beim direkten Schalten über das WebFront wird deshalb die Fehlermeldung `Invalid Ident` ausgelöst. Die Bedienung der Ausgänge funktioniert erst nach einer entsprechenden Erweiterung von `RequestAction()`.
### 7. PHP-Befehlsreferenz ### 7. PHP-Befehlsreferenz
`boolean GEF_BeispielFunktion(integer $InstanzID);` #### PT-Werte manuell einlesen
Erklärung der Funktion.
Beispiel: ```php
`GEF_BeispielFunktion(12345);` GEF_DOUT_ReadPTValues(12345);
```
Liest beide PT-Rohwerte ein, berechnet die Temperaturen und aktualisiert die Variablen `PT1` und `PT2`.
Dabei muss `12345` durch die Objekt-ID der CC100_HW-Instanz ersetzt werden.
#### PT-Werte über RequestAction aktualisieren
```php
IPS_RequestAction(12345, 'DOUT_ReadPTValues', '');
```
#### Digitale Ein- und Ausgänge aktualisieren
```php
IPS_RequestAction(12345, 'UpdateFile', '');
```
Diese Aktion liest die digitalen Eingänge ein und schreibt den aus `Bit1` bis `Bit4` berechneten Ausgangswert in die konfigurierte Systemdatei.
+23
View File
@@ -0,0 +1,23 @@
# Easee SignalR Gateway
Das Gateway hält eine gemeinsame SignalR-Verbindung zum Easee-Konto und verteilt
die Observations 47 (Max Charger Current), 104 (Cable Rating), 109
(Charger Op Mode), 110 (Output Phase), 120 (Total Power) und 182 bis 185
(gemessene Leiterströme) an alle verbundenen
Ladestation- und Ladestation_v2-Instanzen.
Benutzername und Passwort werden ausschließlich im Gateway eingetragen. Mehrere
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.
Jeder Aufruf der Ladestationsinstanz wird sofort als
`set_dynamic_charger_current` an Easee weitergegeben. Wie in der ursprünglichen
Version wird dabei `minutes: 0` gesendet. Das Gateway puffert, begrenzt oder
wiederholt die Stromvorgabe nicht selbst.
Der benötigte WebSocket-Client wird beim Erstellen des Gateways automatisch
und genau einmal angelegt. Nach dem Speichern der Easee-Zugangsdaten wird er
konfiguriert. Im WebSocket-Client muss keine URL eingetragen werden.
+52
View File
@@ -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"
}
]
}
+21
View File
@@ -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": ""
}
+774
View File
@@ -0,0 +1,774 @@
<?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);'
);
// Ein Splitter benötigt einen eigenen I/O-Parent. RequireParent()
// prüft zuerst die bestehende Verbindung und erzeugt daher beim
// Instanzieren keine zweite WebSocket-Verbindung.
$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;
}
if (!$this->EnsureWebSocketParent()) {
$this->SetStatus(203);
return;
}
$this->SetTimerInterval("MaintainConnectionTimer", 10000);
$this->SetTimerInterval("TokenRefreshTimer", 1800000);
if (!$this->RefreshCredentials(false)) {
$this->SetStatus(202);
return;
}
$this->ConnectSignalR();
}
private function EnsureWebSocketParent(): bool
{
$instance = IPS_GetInstance($this->InstanceID);
$parentID = (int)$instance["ConnectionID"];
if ($parentID <= 0 && !$this->RequireParent(self::WEBSOCKET_MODULE_ID)) {
$this->SetLastError("WebSocket-Client konnte nicht erstellt werden");
return false;
}
$instance = IPS_GetInstance($this->InstanceID);
$parentID = (int)$instance["ConnectionID"];
if ($parentID <= 0 || !IPS_InstanceExists($parentID)) {
$this->SetLastError("WebSocket-Client ist nicht verbunden");
return false;
}
$parent = IPS_GetInstance($parentID);
if ($parent["ModuleInfo"]["ModuleID"] !== self::WEBSOCKET_MODULE_ID) {
$this->SetLastError("Ungültige WebSocket-Schnittstelle");
return false;
}
return true;
}
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"]);
}
return $this->ProcessStationRequest($packet["Buffer"]);
}
public function ProcessStationRequest($JSONString)
{
$request = json_decode($JSONString, 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 "SetDynamicChargerCurrent":
case "SetCurrent": // Kompatibilität mit Versionen bis 1.7
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 &&
in_array($observationID, [47, 104, 109, 110, 120, 182, 183, 184, 185], true)
) {
$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] = [];
}
if ($observationID === 109) {
$previousMode = (int)($cache[$serialNumber]["109"] ?? -1);
if ((int)$value === 1 || $previousMode === 1) {
foreach ([110, 182, 183, 184, 185] as $sessionObservationID) {
$cache[$serialNumber][(string)$sessionObservationID] = 0;
}
}
}
$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"];
}
$path = "/api/chargers/" . rawurlencode($serialNumber) .
"/commands/set_dynamic_charger_current";
$body = '{"amps":' . $amps . ',"minutes":0}';
return $this->AuthorizedApiRequest(
"POST",
$path,
$body
);
}
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"),
"application/*+json"
);
if ($response["httpCode"] === 401 && $this->RefreshCredentials(false)) {
$response = $this->HttpRequest(
$method,
self::API_BASE_URL . $path,
$body,
$this->GetBuffer("AccessToken"),
"application/*+json"
);
}
if (!$response["success"]) {
$error = $response["error"];
if ($error === "") {
$error = "Easee-HTTP-Fehler " . $response["httpCode"];
}
return [
"success" => false,
"error" => $error,
"httpCode" => $response["httpCode"],
"body" => $response["body"],
];
}
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 = "",
string $contentType = "application/json"
): array {
$headers = [
"Accept: application/json",
"Content-Type: " . $contentType,
];
if ($bearerToken !== "") {
$headers[] = "Authorization: Bearer " . $bearerToken;
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_ENCODING => "",
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
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);
}
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 : [];
}
}
+25 -55
View File
@@ -1,67 +1,37 @@
# Manager_1 # Energy_Pie
Beschreibung des Moduls.
### Inhaltsverzeichnis IP-Symcon-Modul zur Darstellung von Energieverbrauch und Eigenverbrauch in der Kachelvisualisierung.
1. [Funktionsumfang](#1-funktionsumfang) ## Funktionen
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)
### 1. Funktionsumfang Das Modul zeigt folgende Werte an:
* - Produktion
- Hausverbrauch
- Netzbezug
- Einspeisung
- Eigenverbrauchsquote
- Autarkiegrad
### 2. Voraussetzungen Die Darstellung erfolgt mit zwei Kreisdiagrammen.
## Voraussetzungen
- IP-Symcon ab Version 7.1 - IP-Symcon ab Version 7.1
- Aktivierte Archivierung der verwendeten Energiezähler
- Energiezähler als fortlaufende kWh-Zähler
### 3. Software-Installation ## Konfiguration
* Über den Module Store das 'Manager_1'-Modul installieren. Folgende Variablen müssen ausgewählt werden:
* Alternativ über das Module Control folgende URL hinzufügen
### 4. Einrichten der Instanzen in IP-Symcon | Einstellung | Beschreibung |
|---|---|
| Produktion | Gesamte produzierte Energie in kWh |
| Einspeisung | Ins Netz eingespeiste Energie in kWh |
| Netzbezug | Aus dem Netz bezogene Energie in kWh |
Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. Der Hausverbrauch wird automatisch berechnet:
- Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen)
__Konfigurationsseite__: ```text
Hausverbrauch = Produktion - Einspeisung + Netzbezug
Name | Beschreibung
-------- | ------------------
|
|
### 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);`
+5 -5
View File
@@ -37,14 +37,12 @@
</div> </div>
<style> <style>
@media (max-width:720px){
#grid{grid-template-columns:1fr}
}
#wrap{ #wrap{
position:relative; position:relative;
overflow:hidden; overflow:hidden;
min-height:100vh; overscroll-behavior:none;
touch-action:none;
box-sizing:border-box;
background:#121216; background:#121216;
} }
@@ -213,6 +211,8 @@ function render(data){
elPrev.onclick = ()=>send('Prev',1); elPrev.onclick = ()=>send('Prev',1);
elNext.onclick = ()=>send('Next',1); elNext.onclick = ()=>send('Next',1);
elToday.onclick = ()=>send('Today',1); elToday.onclick = ()=>send('Today',1);
window.addEventListener('load', ()=>send('Today',1));
})(); })();
</script> </script>
+20
View File
@@ -87,10 +87,17 @@ class Energy_Pie extends IPSModule
$dbgProd = []; $dbgProd = [];
$dbgFeed = []; $dbgFeed = [];
$dbgGrid = []; $dbgGrid = [];
if ($range === 'total') {
$prod = $this->readCurrentValue($this->ReadPropertyInteger('VarProduction'), $dbgProd);
$feed = $this->readCurrentValue($this->ReadPropertyInteger('VarFeedIn'), $dbgFeed);
$grid = $this->readCurrentValue($this->ReadPropertyInteger('VarGrid'), $dbgGrid);
$hasData = ($dbgProd['available'] ?? false) || ($dbgFeed['available'] ?? false) || ($dbgGrid['available'] ?? false);
} else {
$prod = $this->readDelta($this->ReadPropertyInteger('VarProduction'), $tStart, $tEnd, $dbgProd); $prod = $this->readDelta($this->ReadPropertyInteger('VarProduction'), $tStart, $tEnd, $dbgProd);
$feed = $this->readDelta($this->ReadPropertyInteger('VarFeedIn'), $tStart, $tEnd, $dbgFeed); $feed = $this->readDelta($this->ReadPropertyInteger('VarFeedIn'), $tStart, $tEnd, $dbgFeed);
$grid = $this->readDelta($this->ReadPropertyInteger('VarGrid'), $tStart, $tEnd, $dbgGrid); $grid = $this->readDelta($this->ReadPropertyInteger('VarGrid'), $tStart, $tEnd, $dbgGrid);
$hasData = (($dbgProd['inRange'] ?? 0) > 0) || (($dbgFeed['inRange'] ?? 0) > 0) || (($dbgGrid['inRange'] ?? 0) > 0); $hasData = (($dbgProd['inRange'] ?? 0) > 0) || (($dbgFeed['inRange'] ?? 0) > 0) || (($dbgGrid['inRange'] ?? 0) > 0);
}
$lastLogTs = 0; $lastLogTs = 0;
if (!$hasData && $range !== 'total') { if (!$hasData && $range !== 'total') {
$lastLogTs = max( $lastLogTs = max(
@@ -160,6 +167,19 @@ class Energy_Pie extends IPSModule
$this->RecalculateAndPush(); $this->RecalculateAndPush();
return $this->GetVisualizationTile(); return $this->GetVisualizationTile();
} }
private function readCurrentValue(int $varId, array &$dbg): float
{
$dbg = ['varId' => $varId, 'available' => false];
if ($varId <= 0 || !IPS_VariableExists($varId)) {
return 0.0;
}
$value = GetValue($varId);
if (!is_numeric($value)) {
return 0.0;
}
$dbg['available'] = true;
return max(0.0, (float)$value);
}
private function readDelta(int $varId, int $tStart, int $tEnd, array &$dbg): float private function readDelta(int $varId, int $tStart, int $tEnd, array &$dbg): float
{ {
$dbg = [ $dbg = [
+32 -130
View File
@@ -1,142 +1,44 @@
# Enelix-Verbrauchermodul Ladestation (EV Charger) # Ladestation
Dieses Modul integriert smarte Ladestationen (Wallboxen) für Elektroautos als steuerbare Verbraucher in das Belevo Energiemanagement-System (Enelix). Es regelt die Ladeleistung dynamisch, unterstützt verschiedene Hersteller-APIs und optimiert das Laden nach PV-Überschuss oder Peak-Shaving-Vorgaben. Das Modul steuert die Ladestationstypen 1 bis 6 mit der ursprünglichen,
gemeinsamen Strom-, Phasen- und Ladeende-Erkennung.
--- ## Gemeinsame Pending-Counter-Logik
## Inhaltsverzeichnis Die Erkennung arbeitet bei allen Stationstypen gleich:
1. [Funktionsumfang](#1-funktionsumfang) 1. Bei einem neu verbundenen Fahrzeug wird zunächst der konfigurierte
2. [Voraussetzungen](#2-voraussetzungen) Maximalstrom an die Station gesendet.
3. [Installation](#3-installation) 2. Nach 60 Sekunden wird anhand der tatsächlichen Ladeleistung zwischen
4. [Instanz anlegen & Konfiguration](#4-instanz-anlegen--konfiguration) einphasigem und dreiphasigem Laden unterschieden.
5. [Statusvariablen & Profile](#5-statusvariablen--profile) 3. Der erreichbare Maximalstrom wird wie ursprünglich aus der Istleistung plus
6. [WebFront / Bedienung](#6-webfront--bedienung) 2,5 A Reserve berechnet und durch `Max_Current_abs` begrenzt.
7. [Mapping auf Code-Komponenten](#7-mapping-auf-code-komponenten) 4. Im laufenden Betrieb vergleicht der `Pending_Counter` Soll- und Istleistung.
8. [Zukünftige Erweiterungen](#8-zukünftige-erweiterungen) Bleibt die Abweichung 90 Sekunden bestehen, wird der Maximalstrom erneut
berechnet.
5. Fällt der berechnete Maximalstrom unter 6 A, wird
`Auto fertig geladen` gesetzt.
--- Eine Sollvorgabe von 0 W setzt den `Pending_Counter` zurück. Die vorhandenen
Leistungsschritte, Mindestlaufzeiten und die Solar-/Peak-Umschaltung entsprechen
der Vorversion.
## 1. Funktionsumfang ## Easee
- **Hersteller-Integration:** Native Unterstützung verschiedener Ladestationen und Cloud-APIs (z.B. Smart-me Pico, Easee, eCarUp, generisches MQTT / REST-API). Die Typen 5 und 6 verwenden dieselbe Logik wie die Typen 1 bis 4. Lediglich die
- **Phasenerkennung:** Das Modul erkennt automatisch (über Leistungsschwellen, z.B. > 7500W), ob das Fahrzeug 1-phasig oder 3-phasig lädt, und passt die internen Strom-zu-Leistung-Umrechnungen an. Kommunikation erfolgt über das `Easee SignalR Gateway`:
- **PowerSteps-Generierung:** Übersetzt die zulässigen Ladeströme (Ampere) in Watt-Schritte (`PowerSteps`), die dem Enelix-Manager für die Energieverteilung angeboten werden (abhängig vom definierten `Mindestaldestrom`, minimal 6A).
- **Verzögerungs- und Schutzlogiken:**
- `Ein_Zeit` / `Aus_Zeit`: Mindestlaufzeiten und Pausenzeiten, um den Schütz im Auto und in der Wallbox vor zu häufigem Schalten zu schützen (Taktschutz).
- `Zeit_Zwischen_Zustandswechseln`: Dämpfung von Leistungssprüngen.
- **Betriebsmodi:**
- *Solarladen:* Automatische Regelung nach PV-Überschuss.
- *Sperr-Priorität:* Regelung nach Peak-Shaving (Netzbezugsgrenzen).
- *Fahrzeugerkennung:* Freigabe der Ladeleistung nur, wenn ein Auto gesteckt (`Car_detected`) und nicht voll (`Car_is_full`) ist.
- **Token-Management:** Automatischer Login und Token-Refresh für Cloud-basierte Systeme (wie Easee, E-Car-Up).
- **Tarifabhängiges Solarladen** Nutzerbasiertes Solarladen mit unterschiedlichen Tarifen für Solar- und Netzladen. Dieese Funktion ist nur über spezielle Token und kostenpflichtigen Konten von drittfirmen aktivierbar. Für weitere Informationen bitte die Belevo AG kontaktieren.
--- - Fahrzeugstatus und Ladeleistung werden aus den Gateway-Observations gelesen.
- Stromvorgaben werden als `SetDynamicChargerCurrent` an das Gateway gesendet.
- Es erfolgt keine interne Anmeldung an der Easee-REST-API.
## 2. Voraussetzungen Typ 5 behält die eCarUp-Benutzerprüfung für die automatische Umschaltung von
`Solarladen`. Typ 6 verwendet keine Benutzerprüfung.
- IP-Symcon **≥ 8.0** ## Einrichtung
- Modul-URL: `https://git.belevo.ch/dh/enelix.git`
- Vorhandener und eingerichteter Enelix Manager.
- Netzwerkzugriff auf die Ladestation (lokale IP) oder aktive Internetverbindung für Cloud-APIs.
--- 1. Eine Instanz `Easee SignalR Gateway` anlegen.
2. Easee-Benutzername und Passwort im Gateway eintragen.
3. In `Ladestation` Typ 5 oder 6 sowie die Easee-Seriennummer wählen.
4. Im Feld `Easee SignalR Gateway` die Gateway-Instanz auswählen.
## 3. Installation
1. In IP-Symcon **Module Control** öffnen.
2. **Hinzufügen → Git-Repository**.
3. URL eingeben und **Installieren**.
4. IP-Symcon neu starten.
---
## 4. Instanz anlegen & Konfiguration
### 4.1 Instanz anlegen
- Rechtsklick **Instanzen** → **Instanz hinzufügen**
- Filter: **Belevo**
- Auswahl: **Ladestation v2 (Verbraucher)**
### 4.2 Properties
| Name | Typ | Beschreibung |
|---|---|---|
| **IP_Adresse** | String | Lokale IP-Adresse der Ladestation (für lokale APIs). |
| **Ladestation** | Select/Int | Auswahl des API-/Hardware-Typs (z.B. 1=MQTT, 2=REST, 3=Pico, 5=eCarUp, 6=Easee). |
| **ID** | String | Stations-ID (z.B. für Smart-me Pico oder eCarUp). |
| **Seriennummer** | String | Seriennummer der Station (z.B. für Easee). |
| **Username** | String | Benutzername für Cloud-APIs. |
| **Password** | String | Passwort für Cloud-APIs. |
| **Interval** | NumberSpinner | Berechnungsintervall und Polling-Rate (Sekunden). |
| **Max_Current_abs** | NumberSpinner | Absoluter Maximalstrom der Zuleitung/Station in Ampere (z.B. 16 oder 32). |
| **Zeit_Zwischen_Zustandswechseln**| NumberSpinner | Verzögerungszeit zwischen zwei Leistungsanpassungen (in Minuten). |
| **Ein_Zeit** | NumberSpinner | Mindestlaufzeit, wenn die Ladung gestartet wird (Minuten). |
| **Aus_Zeit** | NumberSpinner | Mindestpausenzeit nach einem Ladeabbruch (Minuten). |
| **Token_Easee** | SelectVariable | Referenz auf das externe Easee-Token (Variable). |
| **Token_ECarUp** | SelectVariable | Referenz auf das externe eCarUp-Token (Variable). |
| **IdleCounterMax**| NumberSpinner | Anzahl Interval-Zyklen zwischen Leistungsänderungen (Modul-Idle). |
---
## 5. Statusvariablen & Profile
Das Modul verwendet viele interne (versteckte) Variablen für die Manager-Kommunikation. Hier die wichtigsten sichtbaren und steuerbaren Variablen:
| Ident | Typ | Profil | Beschreibung |
|---|---|---|---|
| **Ladebereit** | Boolean | ~Switch | Hauptschalter: Ladestation generell freigegeben oder blockiert. |
| **Solarladen** | Boolean | ~Switch | Aktiviert den reinen PV-Überschusslade-Modus. |
| **Mindestaldestrom** | Integer | - | Minimaler Ladestrom in Ampere (normalerweise mind. 6A für Typ 2). |
| **Is_1_ph** | Boolean | ~Switch | Anzeige: Das Fahrzeug lädt aktuell nur auf einer Phase. |
| **Car_detected** | Boolean | ~Switch | Status: Ein Fahrzeug ist angesteckt und bereit. |
| **Car_is_full** | Boolean | ~Switch | Status: Fahrzeugakku ist voll (Ladestrom < Schwelle). |
| **Pending_Counter** | Integer | Zähler zum Abwarten des Stationszustandes bei Zustandänderung |
| **Leistung_Delta** | Integer | Leistungsabweichung von vorgegebener Soll- und Istleistung |
| **Idle** | Boolean | Status der Sperrzeit (`true` = bereit für neue Werte, `false` = gesperrt). |
| **Ladeleistung_Effektiv**| Float | - | Aktuell gemessene reale Ladeleistung in Watt. |
| **Bezogene_Energie** | Float | - | Rechnerisch aufsummierte Ladeenergie. |
| **Aktuelle_Leistung** | Integer | Die aktuell von der Station erbrachte Leistung. |
| **PowerSteps** | String | JSON-Array der dem Manager angebotenen Leistungsschritte. |
| **Letzer_User** | String | - | Zuletzt authentifizierter RFID-Nutzer (z.B. via eCarUp). |
| **Bezogene_Energie** | Float | Rechnerisch aufsummierte Energie (Leistung × Zeit). |
| **PV_Prio** | Integer | Priorität des Verbrauchers für PV-Optimierung, Tiefe Nummer = Hohe Priorität |
| **Sperre_Prio** | Integer | Priorität des Verbrauchers für Peak-Optimierung, Tiefe Nummer = Hohe Priorität |
| **PV_Prio** | Integer | Priorität des Verbrauchers für PV-Optimierung, Tiefe Nummer = Hohe Priorität |
| **Sperre_Prio** | Integer | Priorität des Verbrauchers für Peak-Optimierung, Tiefe Nummer = Hohe Priorität |
---
## 6. WebFront / Bedienung
Im WebFront bietet die Instanz dem Nutzer Eingriffsmöglichkeiten in das Ladeverhalten:
- **Ladebereit:** Kann genutzt werden, um die Wallbox per Knopfdruck komplett zu deaktivieren.
- **Solarladen:** Zwingt das Modul in den Überschuss-Modus (es wird nur geladen, wenn Leistung durch den Manager zugewiesen wird), Andernfalls wird bei verbundenem Auto solange geladen, bis der Peakshaving-Modus die Leistung reduziert.
- **Mindestaldestrom:** Definiert den Einstiegspunkt für die Ladung (z.B. 6A = ~1,4 kW bei 1-phasig / ~4,1 kW bei 3-phasig).
- **PV_Prio** - Priorität des Verbrauchers für PV-Optimierung, Tiefe Nummer = Hohe Priorität
- **Sperre_Prio** - Priorität des Verbrauchers für Peak-Optimierung, Tiefe Nummer = Hohe Priorität
Zudem dient die Instanz zur Statuskontrolle (Ist ein Auto verbunden? Lädt es? Ist es voll?).
---
## 7. Mapping auf Code-Komponenten
| Komponente | Funktion im Code (`module.php`) |
|---|---|
| **API-Schnittstellen** | `Get_Car_Status($carType)` fragt die jeweilige Hardware ab (cURL an REST/MQTT/Easee) und aktualisiert `Ladeleistung_Effektiv` sowie den `Fahrzeugstatus`. |
| **Autodetektion** | `Detect_Car()` prüft den Zustand, zählt `Pending_Counter` hoch und entscheidet über `Is_1_ph` basierend auf Leistungsgrenzen (7500W). |
| **Leistung → Strom** | `Get_Array_From_Current()` und `Get_Current_From_Power()` rechnen Watt in Ampere um ($P = U \cdot I \cdot Phasen$). |
| **Manager-Interface** | `GetCurrentData($Peak)` baut die `PowerSteps` (z.B. [0, 4140, 4830, ...]) und übergibt diese dem Manager. |
| **Stationssteuerung** | `sendPowerToStation($value)` übersetzt den Soll-Strom (Ampere) in API-spezifische Kommandos (z.B. `amp=16` oder `{"amps":16}`). |
| **Taktschutz (Timers)**| `SetTimerNullMindestlast()`, `SetTimerOn()` und die entsprechenden Reset-Methoden sperren Zustandswechsel nach Start/Stopp. |
| **Token-Refresh** | `Refresh_Token()` erneuert zyklisch das Access-Token (z.B. Easee) über die hinterlegten Credentials. |
---
## 8. Zukünftige Erweiterungen
- **Automatische Phasenumschaltung:** Direkte Ansteuerung von Wallboxen, die eine Umschaltung zwischen 1- und 3-phasigem Laden im laufenden Betrieb unterstützen (zur Erweiterung des Regelbereichs bei wenig Sonne).
+52 -42
View File
@@ -3,19 +3,15 @@
{ {
"type": "Select", "type": "Select",
"name": "Ladestation", "name": "Ladestation",
"caption": "Ladestation Typ", "caption": "Ladestationstyp",
"onChange": "IPS_RequestAction($id, \"UpdateStationForm\", $Ladestation);",
"options": [ "options": [
{
"caption": "Herstellerunabhängig (Ohne interne kommunikation mit Station)",
"value": 4
},
{ {
"caption": "Go-E Wallbox (Alte Version)", "caption": "Go-E Wallbox (Alte Version)",
"value": 1 "value": 1
}, },
{ {
"caption": "Go-E Germini / Germini Flex", "caption": "Go-E Gemini / Gemini Flex",
"value": 2 "value": 2
}, },
{ {
@@ -23,91 +19,105 @@
"value": 3 "value": 3
}, },
{ {
"caption": "Easee", "caption": "Dummy Station (Ohne Ansteuerung)",
"value": 6 "value": 4
}, },
{ {
"caption": "Easee - Nur tarifabhängiges Solarladen", "caption": "Easee - Nur Solarladen",
"value": 5 "value": 5
},
{
"caption": "Easee",
"value": 6
} }
] ]
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "IdleCounterMax", "name": "IdleCounterMax",
"caption": "Anzahl Interval-Zyklen zwischen zwei Leistungsänderungen", "caption": "Wartezyklen nach einer Leistungsänderung",
"suffix": "" "suffix": "Zyklen"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Interval", "name": "Interval",
"caption": "Intervall Neuberechnung der Werte", "caption": "Berechnungsintervall",
"suffix": "Sekunden" "suffix": "Sekunden"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Max_Current_abs", "name": "Max_Current_abs",
"caption": "Maximaler Ladestrom der Station", "caption": "Maximaler Ladestrom",
"suffix": "Ampere" "suffix": "Ampere"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Zeit_Zwischen_Zustandswechseln", "name": "Zeit_Zwischen_Zustandswechseln",
"caption": "Mindestlaufzeit des Verbrauchers bei Lastschaltung (Veraltet, auf 0 lassen, wird demnächst entfehrnt, wird ersetzt durch Mindestlaufzeiten)", "caption": "Mindestzeit zwischen Leistungsänderungen",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Ein_Zeit", "name": "Ein_Zeit",
"caption": "Mindestlaufzeit Ladestation wenn Ein", "caption": "Mindestlaufzeit nach dem Einschalten",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Aus_Zeit", "name": "Aus_Zeit",
"caption": "Mindestlaufzeit Ladestation wenn Aus", "caption": "Mindeststillstandszeit nach dem Ausschalten",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"name": "IP_Adresse", "name": "IP_Adresse",
"caption": "IP-Adresse Ladestation (für Go-E Modelle)" "caption": "IP-Adresse",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Geräte-ID (für Smart-Me / ECarUp / Easee)", "name": "ID",
"name": "ID" "caption": "Geräte-ID",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Seriennummer (für Smart-Me / ECarUp / Easee)", "name": "Seriennummer",
"name": "Seriennummer" "caption": "Seriennummer",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Username / Benutzernahme (für Easee, Smart-Me, )", "name": "Username",
"name": "Username" "caption": "Benutzername",
"visible": false
},
{ "type": "PasswordTextBox",
"caption": "Password -> (Smart-Me)",
"name": "Password"
}, },
{ {
"type": "SelectVariable", "type": "PasswordTextBox",
"name": "Token_Easee", "name": "Password",
"caption": "Variable API-Token für Easee-Solarladen", "caption": "Passwort",
"suffix": "" "visible": false
}, },
{ {
"type": "SelectVariable", "type": "SelectVariable",
"name": "Token_ECarUp", "name": "Token_ECarUp",
"caption": "Variable API-Token für ECarUp (Easee Solarladen)", "caption": "Variable mit eCarUp API-Token",
"suffix": "" "suffix": "",
"visible": false
},
{
"type": "SelectInstance",
"name": "EaseeGatewayID",
"caption": "Easee SignalR Gateway",
"visible": false
},
{
"type": "Label",
"name": "EaseeGatewayHint",
"caption": "Die Easee-Zugangsdaten werden im ausgewählten Gateway konfiguriert.",
"visible": false
} }
] ]
} }
+2
View File
@@ -10,3 +10,5 @@
"prefix": "GEF", "prefix": "GEF",
"url": "https://git.belevo.ch/dh/Enelix/src/branch/main/Ladestation" "url": "https://git.belevo.ch/dh/Enelix/src/branch/main/Ladestation"
} }
+449 -155
View File
@@ -1,6 +1,10 @@
<?php <?php
class Ladestation extends IPSModule class Ladestation extends IPSModule
{ {
private const EASEE_GATEWAY_MODULE_ID = "{7A1F1D4B-3E7D-4A3B-9E54-61C8C3B7F201}";
private const EASEE_GATEWAY_REQUEST_ID = "{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}";
private const EASEE_GATEWAY_EVENT_ID = "{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}";
public function Create() public function Create()
{ {
parent::Create(); parent::Create();
@@ -17,7 +21,8 @@ class Ladestation extends IPSModule
$this->RegisterPropertyInteger("Ein_Zeit", 0); // Recheninterval $this->RegisterPropertyInteger("Ein_Zeit", 0); // Recheninterval
$this->RegisterPropertyInteger("Aus_Zeit", 0); // Recheninterval $this->RegisterPropertyInteger("Aus_Zeit", 0); // Recheninterval
$this->RegisterPropertyInteger("Token_Easee", 0); // Recheninterval $this->RegisterPropertyInteger("Token_Easee", 0); // Recheninterval
$this->RegisterPropertyInteger("Token_ECarUp", 0); // Recheninterval $this->RegisterPropertyInteger("Token_ECarUp", 0);
$this->RegisterPropertyInteger("EaseeGatewayID", 0); // Recheninterval
// Ladestationspezifische Variabeln // Ladestationspezifische Variabeln
$this->RegisterVariableFloat("Max_Current", "Maximaler Ladestrom", "", 0); $this->RegisterVariableFloat("Max_Current", "Maximaler Ladestrom", "", 0);
IPS_SetHidden($this->GetIDForIdent("Max_Current") , true); IPS_SetHidden($this->GetIDForIdent("Max_Current") , true);
@@ -81,18 +86,178 @@ class Ladestation extends IPSModule
public function ApplyChanges() public function ApplyChanges()
{ {
parent::ApplyChanges(); parent::ApplyChanges();
$this->SetTimerInterval("Timer_Do_UserCalc_EVC", $this->ReadPropertyInteger("Interval") * 1000); $this->SetTimerInterval(
// erstelle einen Timer der das Request token aktualisiert wenn die station dies braucht (aktuell nur für Solarladelösung Easee). "Timer_Do_UserCalc_EVC",
if ($this->ReadPropertyInteger("Ladestation") == 6) $this->ReadPropertyInteger("Interval") * 1000
);
$stationType = $this->ReadPropertyInteger("Ladestation");
if (!$this->UsesEaseeGateway($stationType)) {
$this->SetStatus(102);
return;
}
$this->SetTimerInterval("Timer_Refresh_Token", 0);
if ($this->GetConfiguredEaseeGatewayID() <= 0) {
$this->SetStatus(102);
return;
}
try {
$this->SubscribeToEaseeGateway();
$this->GetEaseeStateFromGateway();
$this->SetStatus(102);
} catch (Throwable $exception) {
$this->SetStatus(102);
return;
}
}
public function GetConfigurationForm()
{ {
$this->Refresh_Token(); $form = json_decode(
$this->SetTimerInterval("Timer_Refresh_Token", 1800000); file_get_contents(__DIR__ . "/form.json"),
true
);
$fieldConfiguration = $this->GetStationFormConfiguration(
$this->ReadPropertyInteger("Ladestation")
);
foreach ($form["elements"] as &$element) {
$name = (string)($element["name"] ?? "");
if (!isset($fieldConfiguration[$name])) {
continue;
}
foreach ($fieldConfiguration[$name] as $parameter => $value) {
$element[$parameter] = $value;
} }
} }
unset($element);
return json_encode($form);
}
private function UpdateStationForm(int $stationType)
{
foreach (
$this->GetStationFormConfiguration($stationType)
as $field => $parameters
) {
foreach ($parameters as $parameter => $value) {
$this->UpdateFormField($field, $parameter, $value);
}
}
}
private function GetStationFormConfiguration(int $stationType): array
{
$configuration = [
"IP_Adresse" => [
"visible" => false,
"caption" => "IP-Adresse",
],
"ID" => [
"visible" => false,
"caption" => "Geräte-ID",
],
"Seriennummer" => [
"visible" => false,
"caption" => "Seriennummer",
],
"Username" => [
"visible" => false,
"caption" => "Benutzername",
],
"Password" => [
"visible" => false,
"caption" => "Passwort",
],
"Token_ECarUp" => [
"visible" => false,
"caption" => "Variable mit eCarUp API-Token",
],
"EaseeGatewayID" => [
"visible" => false,
],
"EaseeGatewayHint" => [
"visible" => false,
],
];
switch ($stationType) {
case 1:
$configuration["IP_Adresse"] = [
"visible" => true,
"caption" => "IP-Adresse der Go-E Wallbox",
];
break;
case 2:
$configuration["IP_Adresse"] = [
"visible" => true,
"caption" => "IP-Adresse der Go-E Gemini / Gemini Flex",
];
break;
case 3:
$configuration["ID"] = [
"visible" => true,
"caption" => "Smart-Me Geräte-ID",
];
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Smart-Me Seriennummer",
];
$configuration["Username"] = [
"visible" => true,
"caption" => "Smart-Me Benutzername",
];
$configuration["Password"] = [
"visible" => true,
"caption" => "Smart-Me Passwort",
];
break;
case 5:
$configuration["ID"] = [
"visible" => true,
"caption" => "eCarUp Stations-/Connector-ID",
];
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Easee Seriennummer",
];
$configuration["Username"] = [
"visible" => true,
"caption" =>
"eCarUp Benutzerkennung für Solarladen",
];
$configuration["Token_ECarUp"]["visible"] = true;
$configuration["EaseeGatewayID"]["visible"] = true;
$configuration["EaseeGatewayHint"]["visible"] = true;
break;
case 6:
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Easee Seriennummer",
];
$configuration["EaseeGatewayID"]["visible"] = true;
$configuration["EaseeGatewayHint"]["visible"] = true;
break;
}
return $configuration;
}
public function RequestAction($Ident, $Value) public function RequestAction($Ident, $Value)
{ {
switch ($Ident) switch ($Ident)
{ {
case "UpdateStationForm":
$this->UpdateStationForm((int)$Value);
break;
case "SetAktuelle_Leistung": case "SetAktuelle_Leistung":
$this->SetValue("Power", (int)$Value); $this->SetValue("Power", (int)$Value);
break; break;
@@ -331,76 +496,45 @@ class Ladestation extends IPSModule
$car_on_station = true; $car_on_station = true;
break; break;
case 5: // Solarladen E-Car-Up case 5: // Solarladen E-Car-Up
// Benutzer eCarUp abfragen
$authToken = GetValue($this->ReadPropertyInteger("Token_ECarUp")); $authToken = GetValue($this->ReadPropertyInteger("Token_ECarUp"));
$ID = $this->ReadPropertyString("ID"); $ID = $this->ReadPropertyString("ID");
$apiUrl = 'https://public-api.ecarup.com/v1/station/' . $ID . '/conn' . 'ectors/' . $ID . '/active-charging'; $apiUrl = "https://public-api.ecarup.com/v1/station/" . $ID .
$ch = curl_init(); "/connectors/" . $ID . "/active-charging";
curl_setopt($ch, CURLOPT_URL, $apiUrl); $eCarUpHandle = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($eCarUpHandle, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['accept: application/json', 'Authorization: Bearer ' . $authToken]); curl_setopt($eCarUpHandle, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch); curl_setopt($eCarUpHandle, CURLOPT_HTTPHEADER, [
$data = json_decode($response, true); "accept: application/json",
curl_close($ch); "Authorization: Bearer " . $authToken,
if (json_last_error() === JSON_ERROR_NONE && isset($data["driverIdentifier"])) ]);
{ $eCarUpResponse = curl_exec($eCarUpHandle);
$this->SetValue("Letzer_User", $data["driverIdentifier"]); $eCarUpData = json_decode($eCarUpResponse, true);
if ($this->ReadPropertyString("Username") === $data["driverIdentifier"]) curl_close($eCarUpHandle);
{
$this->SetValue("Solarladen", true); if (
} json_last_error() === JSON_ERROR_NONE &&
else isset($eCarUpData["driverIdentifier"])
{ ) {
$this->SetValue("Solarladen", false); $this->SetValue(
} "Letzer_User",
} $eCarUpData["driverIdentifier"]
//Aktueller Zustand Ladestation abfragen (Leistung, auto eingesteckt?) );
$ch2 = curl_init(); $this->SetValue(
$url2 = "https://api.easee.com/api/chargers/" . $this->ReadPropertyString("Seriennummer") . "/state"; "Solarladen",
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"], ]); $this->ReadPropertyString("Username") ===
curl_setopt($ch2, CURLOPT_URL, $url2); $eCarUpData["driverIdentifier"]
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; break;
case 6: // Easee case 6: // Easee
//Aktueller Zustand Ladestation abfragen (Leistung, auto eingesteckt?) $car_on_station =
$ch2 = curl_init(); $this->GetEaseeCarStatusFromGateway($car_on_station);
$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"]);
}
break; break;
default: default:
$this->SetValue("Car_detected", false); $this->SetValue("Car_detected", false);
$this->SetValue("Fahrzeugstatus", -1); $this->SetValue("Fahrzeugstatus", -1);
@@ -409,6 +543,7 @@ class Ladestation extends IPSModule
return $car_on_station; return $car_on_station;
} }
// Methode zum Berechnen des Maximalen Stroms den eine Ladestation/Auto ziehen kann. // Methode zum Berechnen des Maximalen Stroms den eine Ladestation/Auto ziehen kann.
public function Calc_Max_Current(bool $is_1_ph) public function Calc_Max_Current(bool $is_1_ph)
{ {
$maxCurrent = 32; $maxCurrent = 32;
@@ -645,62 +780,51 @@ class Ladestation extends IPSModule
{ {
$baseUrl = ""; $baseUrl = "";
$stationType = $this->ReadPropertyInteger("Ladestation"); $stationType = $this->ReadPropertyInteger("Ladestation");
switch ($stationType)
{ if ($stationType === 5 || $stationType === 6) {
case 1: // GO-E alt if (
$stationType === 5 &&
$this->ReadPropertyString("Username") != $this->GetValue("Letzer_User")
) {
return;
}
return $this->SendEaseeCurrentToGateway($value);
}
switch ($stationType) {
case 1:
$baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/mqtt?payload="; $baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/mqtt?payload=";
break; break;
case 2: // GO-E Gemini case 2:
$baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/api/set?"; $baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/api/set?";
break; break;
case 3: // Smart Me Pico case 3:
$baseUrl = "https://api.smart-me.com/pico/loadmanagementgroup/current/" . $this->ReadPropertyString("Seriennummer") . "?current="; $baseUrl = "https://api.smart-me.com/pico/loadmanagementgroup/current/" . $this->ReadPropertyString("Seriennummer") . "?current=";
break; break;
case 4: // Dummy case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; return;
case 5: // Easee Solar
// Keine base Url nötig
break;
case 6: // Easee
// Keine base Url nötig
break;
} }
$ch = curl_init(); $ch = curl_init();
IPS_LogMessage("value", "nach konvertierung" . $value); if ($value == 0) {
if ($value == 0) switch ($stationType) {
{ case 1:
switch ($stationType)
{
case 1: // GO-E alt
$url = $baseUrl . "alw=0"; $url = $baseUrl . "alw=0";
break; break;
case 2: // GO-E Gemini case 2:
$url = $baseUrl . "frc=1&sua=false"; $url = $baseUrl . "frc=1&sua=false";
break; break;
case 3: // Smart Me Pico case 3:
$url = $baseUrl . "0"; $url = $baseUrl . "0";
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $this->ReadPropertyString("Username") . ":" . $this->ReadPropertyString("Password")); curl_setopt($ch, CURLOPT_USERPWD, $this->ReadPropertyString("Username") . ":" . $this->ReadPropertyString("Password"));
break; break;
case 4: // Dummy case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; return;
case 5: // Easee Solar
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: // Easee
$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: default:
return "Invalid station type."; return "Invalid station type.";
} }
@@ -709,94 +833,264 @@ class Ladestation extends IPSModule
$response = curl_exec($ch); $response = curl_exec($ch);
curl_close($ch); curl_close($ch);
return $response; return $response;
} } elseif ($value >= 1 && $value <= 32) {
elseif ($value >= 1 && $value <= 32) switch ($stationType) {
{ case 1:
switch ($stationType)
{
case 1: // Go-E Alt
$url1 = $baseUrl . "alw=1"; $url1 = $baseUrl . "alw=1";
$url2 = $baseUrl . "amp=$value"; $url2 = $baseUrl . "amp=$value";
break; break;
case 2: // Go-E Gemini case 2:
$url1 = $baseUrl . "frc=2"; $url1 = $baseUrl . "frc=2";
$url2 = $baseUrl . "amp=$value"; $url2 = $baseUrl . "amp=$value";
break; break;
case 3: // Pico case 3:
$url2 = $baseUrl . ($value * 1000); $url2 = $baseUrl . ($value * 1000);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, $this->ReadPropertyString("Username") . ":" . $this->ReadPropertyString("Password")); curl_setopt($ch, CURLOPT_USERPWD, $this->ReadPropertyString("Username") . ":" . $this->ReadPropertyString("Password"));
break; break;
case 4: //Dummy case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; return;
case 5: // Easee solar
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: // Easee
$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: default:
return "Invalid station type."; return "Invalid station type.";
} }
if (isset($url1))
{ if (isset($url1)) {
curl_setopt($ch, CURLOPT_URL, $url1); curl_setopt($ch, CURLOPT_URL, $url1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response1 = curl_exec($ch); $response1 = curl_exec($ch);
if (curl_errno($ch)) if (curl_errno($ch)) {
{
curl_close($ch); curl_close($ch);
return "Error:" . curl_error($ch); return "Error:" . curl_error($ch);
} }
} }
curl_setopt($ch, CURLOPT_URL, $url2); curl_setopt($ch, CURLOPT_URL, $url2);
$response2 = curl_exec($ch); $response2 = curl_exec($ch);
if (curl_errno($ch)) if (curl_errno($ch)) {
{
curl_close($ch); curl_close($ch);
return "Error:" . curl_error($ch); return "Error:" . curl_error($ch);
} }
curl_close($ch); curl_close($ch);
return; return;
} } else {
else
{
return "Invalid value. Must be between 0 and 32."; return "Invalid value. Must be between 0 and 32.";
} }
} }
// Token für Solarladen Easee neu holen
public function Refresh_Token() public function Refresh_Token()
{ {
$payload = json_encode(['userName' => $this->ReadPropertyString("Username") , 'password' => $this->ReadPropertyString("Password") , ]); // Easee-Zugangsdaten und Token werden ausschliesslich
// cURL-Handle initialisieren // im SignalR-Gateway verwaltet.
$ch = curl_init('https://api.easee.com/api/accounts/login'); return;
// cURL-Optionen setzen }
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, // Antwort als String zurückgeben
CURLOPT_POST => true, // POST-Methode verwenden private function UsesEaseeGateway(int $stationType): bool
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); return $stationType === 5 || $stationType === 6;
curl_close($ch);
exit;
} }
// cURL-Handle schließen
curl_close($ch); private function IsEaseeGatewayInstance(int $instanceID): bool
$this->SetBuffer("Token_Intern", (json_decode($response, true) ['accessToken'])); {
if ($instanceID <= 0 || !IPS_InstanceExists($instanceID)) {
return false;
} }
// Intern Idle counter zählen
$instance = IPS_GetInstance($instanceID);
return
$instance["ModuleInfo"]["ModuleID"] ===
self::EASEE_GATEWAY_MODULE_ID;
}
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)
{
$gatewayID = $this->GetConfiguredEaseeGatewayID();
if (
$gatewayID <= 0 ||
!function_exists("EASEEGW_ProcessStationRequest")
) {
return null;
}
$response = EASEEGW_ProcessStationRequest(
$gatewayID,
json_encode($request)
);
if (!is_string($response) || $response === "") {
return null;
}
$decoded = json_decode($response, true);
return is_array($decoded) ? $decoded : null;
}
private function GetConfiguredEaseeGatewayID(): int
{
$gatewayID = $this->ReadPropertyInteger("EaseeGatewayID");
if (!$this->IsEaseeGatewayInstance($gatewayID)) {
return 0;
}
return $gatewayID;
}
private function ApplyEaseeGatewayResponse($response)
{
if (!is_array($response)) {
return;
}
if (!isset($response["state"]) || !is_array($response["state"])) {
return;
}
$updated = (int)($response["state"]["updated"] ?? time());
foreach (
[47, 104, 109, 110, 120, 182, 183, 184, 185]
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 (
!in_array(
$observationID,
[47, 104, 109, 110, 120, 182, 183, 184, 185],
true
)
) {
return;
}
$state = json_decode(
$this->GetBuffer("EaseeGatewayState"),
true
);
if (!is_array($state)) {
$state = [];
}
if ($observationID === 109) {
$previousMode = (int)($state["109"] ?? -1);
if ((int)$value === 1 || $previousMode === 1) {
foreach (
[110, 182, 183, 184, 185]
as $sessionObservationID
) {
$state[(string)$sessionObservationID] = 0;
}
}
}
$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"];
$currentState =
$chargerOpMode != 1 &&
($chargerOpMode != 6 || $this->GetValue("Car_detected")) &&
$chargerOpMode != 7;
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" => "SetDynamicChargerCurrent",
"serialNumber" => strtoupper(
trim($this->ReadPropertyString("Seriennummer"))
),
"amps" => $value,
]);
if (!is_array($response) || !($response["success"] ?? false)) {
return is_array($response)
? (string)(
$response["error"] ??
"Unbekannter Gateway-Fehler"
)
: "Easee Gateway nicht erreichbar";
}
return $response["body"] ?? null;
}
public function CheckIdle($power) public function CheckIdle($power)
{ {
$lastpower = GetValue($this->GetIDForIdent("Aktuelle_Leistung")); $lastpower = GetValue($this->GetIDForIdent("Aktuelle_Leistung"));
+32 -55
View File
@@ -1,67 +1,44 @@
# Manager_1 # Ladestation_v2
Beschreibung des Moduls.
### Inhaltsverzeichnis Das Modul steuert die Ladestationstypen 1 bis 6 mit der ursprünglichen,
gemeinsamen Strom-, Phasen- und Ladeende-Erkennung.
1. [Funktionsumfang](#1-funktionsumfang) ## Gemeinsame Pending-Counter-Logik
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)
### 1. Funktionsumfang Die Erkennung arbeitet bei allen Stationstypen gleich:
* 1. Bei einem neu verbundenen Fahrzeug wird zunächst der konfigurierte
Maximalstrom an die Station gesendet.
2. Nach 60 Sekunden wird anhand der tatsächlichen Ladeleistung zwischen
einphasigem und dreiphasigem Laden unterschieden.
3. Der erreichbare Maximalstrom wird wie ursprünglich aus der Istleistung plus
2,5 A Reserve berechnet und durch `Max_Current_abs` begrenzt.
4. Im laufenden Betrieb vergleicht der `Pending_Counter` Soll- und Istleistung.
Bleibt die Abweichung 90 Sekunden bestehen, wird der Maximalstrom erneut
berechnet.
5. Fällt der berechnete Maximalstrom unter 6 A, wird
`Auto fertig geladen` gesetzt.
### 2. Voraussetzungen Eine Sollvorgabe von 0 W setzt den `Pending_Counter` zurück. Die vorhandenen
Leistungsschritte, Mindestlaufzeiten und die Solar-/Peak-Umschaltung entsprechen
der Vorversion.
- IP-Symcon ab Version 7.1 ## Easee
### 3. Software-Installation Die Typen 5 und 6 verwenden dieselbe Logik wie die Typen 1 bis 4. Lediglich die
Kommunikation erfolgt über das oben ausgewählte `Easee SignalR Gateway`:
* Über den Module Store das 'Manager_1'-Modul installieren. - Fahrzeugstatus und Ladeleistung werden aus den Gateway-Observations gelesen.
* Alternativ über das Module Control folgende URL hinzufügen - Stromvorgaben werden als `SetDynamicChargerCurrent` an das Gateway gesendet.
- Es erfolgt keine interne Anmeldung an der Easee-REST-API.
### 4. Einrichten der Instanzen in IP-Symcon Typ 5 behält die eCarUp-Benutzerprüfung für die automatische Umschaltung von
`Solarladen`. Typ 6 verwendet keine Benutzerprüfung.
Unter 'Instanz hinzufügen' kann das 'Manager_1'-Modul mithilfe des Schnellfilters gefunden werden. ## Einrichtung
- Weitere Informationen zum Hinzufügen von Instanzen in der [Dokumentation der Instanzen](https://www.symcon.de/service/dokumentation/konzepte/instanzen/#Instanz_hinzufügen)
__Konfigurationsseite__: 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. Das Gateway oben über `Schnittstelle konfigurieren/ändern` auswählen.
Name | Beschreibung
-------- | ------------------
|
|
### 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);`
+38 -33
View File
@@ -3,14 +3,15 @@
{ {
"type": "Select", "type": "Select",
"name": "Ladestation", "name": "Ladestation",
"caption": "Ladestation", "caption": "Ladestationstyp",
"onChange": "IPS_RequestAction($id, \"UpdateStationForm\", $Ladestation);",
"options": [ "options": [
{ {
"caption": "Go-E Wallbox (Alte Version)", "caption": "Go-E Wallbox (Alte Version)",
"value": 1 "value": 1
}, },
{ {
"caption": "Go-E Germini / Germini Flex", "caption": "Go-E Gemini / Gemini Flex",
"value": 2 "value": 2
}, },
{ {
@@ -34,13 +35,13 @@
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "IdleCounterMax", "name": "IdleCounterMax",
"caption": "Zyklen zwischen zwei Leistungsänderungen (Multipliziert sich mit Interval)", "caption": "Wartezyklen nach einer Leistungsänderung",
"suffix": "" "suffix": "Zyklen"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Interval", "name": "Interval",
"caption": "Intervall Neuberechnung der Werte", "caption": "Berechnungsintervall",
"suffix": "Sekunden" "suffix": "Sekunden"
}, },
{ {
@@ -52,61 +53,65 @@
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Zeit_Zwischen_Zustandswechseln", "name": "Zeit_Zwischen_Zustandswechseln",
"caption": "(Veraltet, auf 0 lassen, wird in nächster Version entfehrnt) Mindestlaufzeit des Verbrauchers bei Lastschaltung ", "caption": "Mindestzeit zwischen Leistungsänderungen",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Ein_Zeit", "name": "Ein_Zeit",
"caption": "Mindestlaufzeit Ladestation wenn Ein", "caption": "Mindestlaufzeit nach dem Einschalten",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "Aus_Zeit", "name": "Aus_Zeit",
"caption": "Mindestlaufzeit Ladestation wenn Aus", "caption": "Mindeststillstandszeit nach dem Ausschalten",
"suffix": "" "suffix": "Minuten"
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"name": "IP_Adresse", "name": "IP_Adresse",
"caption": "IP-Adresse Ladestation" "caption": "IP-Adresse",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Geräte-ID Smart-Me / ECarUp / Easee", "name": "ID",
"name": "ID" "caption": "Geräte-ID",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Seriennummer Smart-Me / ECarUp / Easee", "name": "Seriennummer",
"name": "Seriennummer" "caption": "Seriennummer",
"visible": false
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"caption": "Username / Benutzernahme Solarladen", "name": "Username",
"name": "Username" "caption": "Benutzername",
"visible": false
},
{ "type": "PasswordTextBox",
"caption": "Password -> Bei Anwendung mit Pico-Stationen",
"name": "Password"
}, },
{ {
"type": "SelectVariable", "type": "PasswordTextBox",
"name": "Token_Easee", "name": "Password",
"caption": "Variable API-Token für Easee", "caption": "Passwort",
"suffix": "" "visible": false
}, },
{ {
"type": "SelectVariable", "type": "SelectVariable",
"name": "Token_ECarUp", "name": "Token_ECarUp",
"caption": "Variable API-Token für ECarUp", "caption": "Variable mit eCarUp API-Token",
"suffix": "" "suffix": "",
"visible": false
},
{
"type": "Label",
"name": "EaseeGatewayHint",
"caption": "Das Easee SignalR Gateway wird oben über Schnittstelle konfigurieren/ändern ausgewählt.",
"visible": false
} }
] ]
} }
+8 -2
View File
@@ -4,9 +4,15 @@
"type": 3, "type": 3,
"vendor": "Belevo AG", "vendor": "Belevo AG",
"aliases": [], "aliases": [],
"parentRequirements": [], "parentRequirements": [
"{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}"
],
"childRequirements": [], "childRequirements": [],
"implemented": [], "implemented": [
"{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}"
],
"prefix": "GEF", "prefix": "GEF",
"url": "" "url": ""
} }
+439 -223
View File
@@ -2,6 +2,10 @@
class Ladestation_v2 extends IPSModule 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() public function Create()
{ {
@@ -20,7 +24,8 @@ class Ladestation_v2 extends IPSModule
$this->RegisterPropertyInteger("Ein_Zeit", 0); // Recheninterval $this->RegisterPropertyInteger("Ein_Zeit", 0); // Recheninterval
$this->RegisterPropertyInteger("Aus_Zeit", 0); // Recheninterval $this->RegisterPropertyInteger("Aus_Zeit", 0); // Recheninterval
$this->RegisterPropertyInteger("Token_Easee", 0); // Recheninterval $this->RegisterPropertyInteger("Token_Easee", 0); // Recheninterval
$this->RegisterPropertyInteger("Token_ECarUp", 0); // Recheninterval $this->RegisterPropertyInteger("Token_ECarUp", 0);
$this->RegisterPropertyInteger("EaseeGatewayID", 0); // Recheninterval
// Ladestationspezifische Variabeln // Ladestationspezifische Variabeln
@@ -110,23 +115,210 @@ class Ladestation_v2 extends IPSModule
public function ApplyChanges() public function ApplyChanges()
{ {
parent::ApplyChanges(); parent::ApplyChanges();
$this->SetTimerInterval("Timer_Do_UserCalc_EVC",$this->ReadPropertyInteger("Interval")*1000); $this->SetTimerInterval(
"Timer_Do_UserCalc_EVC",
// erstelle einen Timer der das Request token aktualisiert wenn die station dies braucht. $this->ReadPropertyInteger("Interval") * 1000
if($this->ReadPropertyInteger("Ladestation")==6){ );
$this->Refresh_Token();
$this->SetTimerInterval("Timer_Refresh_Token",1800000);
$stationType = $this->ReadPropertyInteger("Ladestation");
if (!$this->UsesEaseeGateway($stationType)) {
$this->SetStatus(102);
return;
} }
// Zusätzliche Anpassungen nach Bedarf $this->SetTimerInterval("Timer_Refresh_Token", 0);
try {
$instance = IPS_GetInstance($this->InstanceID);
$parentID = (int)$instance["ConnectionID"];
if ($parentID <= 0) {
$this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID);
$instance = IPS_GetInstance($this->InstanceID);
$parentID = (int)$instance["ConnectionID"];
} }
if (!$this->IsEaseeGatewayInstance($parentID)) {
$this->SetStatus(102);
return;
}
$this->SubscribeToEaseeGateway();
$this->GetEaseeStateFromGateway();
$this->SetStatus(102);
} catch (Throwable $exception) {
$this->SetStatus(102);
return;
}
}
public function GetConfigurationForm()
{
$form = json_decode(
file_get_contents(__DIR__ . "/form.json"),
true
);
$fieldConfiguration = $this->GetStationFormConfiguration(
$this->ReadPropertyInteger("Ladestation")
);
foreach ($form["elements"] as &$element) {
$name = (string)($element["name"] ?? "");
if (!isset($fieldConfiguration[$name])) {
continue;
}
foreach ($fieldConfiguration[$name] as $parameter => $value) {
$element[$parameter] = $value;
}
}
unset($element);
return json_encode($form);
}
private function UpdateStationForm(int $stationType)
{
foreach (
$this->GetStationFormConfiguration($stationType)
as $field => $parameters
) {
foreach ($parameters as $parameter => $value) {
$this->UpdateFormField($field, $parameter, $value);
}
}
}
private function GetStationFormConfiguration(int $stationType): array
{
$configuration = [
"IP_Adresse" => [
"visible" => false,
"caption" => "IP-Adresse",
],
"ID" => [
"visible" => false,
"caption" => "Geräte-ID",
],
"Seriennummer" => [
"visible" => false,
"caption" => "Seriennummer",
],
"Username" => [
"visible" => false,
"caption" => "Benutzername",
],
"Password" => [
"visible" => false,
"caption" => "Passwort",
],
"Token_ECarUp" => [
"visible" => false,
"caption" => "Variable mit eCarUp API-Token",
],
"EaseeGatewayHint" => [
"visible" => false,
],
];
switch ($stationType) {
case 1:
$configuration["IP_Adresse"] = [
"visible" => true,
"caption" => "IP-Adresse der Go-E Wallbox",
];
break;
case 2:
$configuration["IP_Adresse"] = [
"visible" => true,
"caption" => "IP-Adresse der Go-E Gemini / Gemini Flex",
];
break;
case 3:
$configuration["ID"] = [
"visible" => true,
"caption" => "Smart-Me Geräte-ID",
];
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Smart-Me Seriennummer",
];
$configuration["Username"] = [
"visible" => true,
"caption" => "Smart-Me Benutzername",
];
$configuration["Password"] = [
"visible" => true,
"caption" => "Smart-Me Passwort",
];
break;
case 5:
$configuration["ID"] = [
"visible" => true,
"caption" => "eCarUp Stations-/Connector-ID",
];
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Easee Seriennummer",
];
$configuration["Username"] = [
"visible" => true,
"caption" =>
"eCarUp Benutzerkennung für Solarladen",
];
$configuration["Token_ECarUp"]["visible"] = true;
$configuration["EaseeGatewayHint"]["visible"] = true;
break;
case 6:
$configuration["Seriennummer"] = [
"visible" => true,
"caption" => "Easee Seriennummer",
];
$configuration["EaseeGatewayHint"]["visible"] = true;
break;
}
return $configuration;
}
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") {
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())
);
}
public function RequestAction($Ident, $Value) public function RequestAction($Ident, $Value)
{ {
switch ($Ident) { switch ($Ident) {
case "UpdateStationForm":
$this->UpdateStationForm((int)$Value);
break;
case "SetAktuelle_Leistung": case "SetAktuelle_Leistung":
$this->SetValue("Power", (int)$Value); $this->SetValue("Power", (int)$Value);
@@ -406,118 +598,47 @@ class Ladestation_v2 extends IPSModule
case 4: case 4:
$car_on_station = true; $car_on_station = true;
break; break;
case 5: // Solarladen E-Car-Up
case 5:
// Benutzer eCarUp abfragen
$authToken = GetValue($this->ReadPropertyInteger("Token_ECarUp")); $authToken = GetValue($this->ReadPropertyInteger("Token_ECarUp"));
$ID = $this->ReadPropertyString("ID"); $ID = $this->ReadPropertyString("ID");
$apiUrl = 'https://public-api.ecarup.com/v1/station/' . $ID . '/conn'.'ectors/'. $ID . '/active-charging'; $apiUrl = "https://public-api.ecarup.com/v1/station/" . $ID .
$ch = curl_init(); "/connectors/" . $ID . "/active-charging";
curl_setopt($ch, CURLOPT_URL, $apiUrl); $eCarUpHandle = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($eCarUpHandle, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, [ curl_setopt($eCarUpHandle, CURLOPT_RETURNTRANSFER, true);
'accept: application/json', curl_setopt($eCarUpHandle, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $authToken "accept: application/json",
"Authorization: Bearer " . $authToken,
]); ]);
$eCarUpResponse = curl_exec($eCarUpHandle);
$eCarUpData = json_decode($eCarUpResponse, true);
curl_close($eCarUpHandle);
$response = curl_exec($ch); if (
$data = json_decode($response, true); json_last_error() === JSON_ERROR_NONE &&
isset($eCarUpData["driverIdentifier"])
curl_close($ch); ) {
$this->SetValue(
if (json_last_error() === JSON_ERROR_NONE && isset($data["driverIdentifier"])) { "Letzer_User",
$eCarUpData["driverIdentifier"]
$this->SetValue("Letzer_User", $data["driverIdentifier"]); );
$this->SetValue(
if($this->ReadPropertyString("Username") === $data["driverIdentifier"]){ "Solarladen",
$this->SetValue("Solarladen", true); $this->ReadPropertyString("Username") ===
}else{ $eCarUpData["driverIdentifier"]
$this->SetValue("Solarladen", false); );
}
}
//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; break;
case 6: case 6: // Easee
$car_on_station =
$this->GetEaseeCarStatusFromGateway($car_on_station);
//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"]);
}
break; break;
default: default:
$this->SetValue("Car_detected", false); $this->SetValue("Car_detected", false);
$this->SetValue("Fahrzeugstatus", -1); $this->SetValue("Fahrzeugstatus", -1);
@@ -759,6 +880,16 @@ class Ladestation_v2 extends IPSModule
$baseUrl = ""; $baseUrl = "";
$stationType = $this->ReadPropertyInteger("Ladestation"); $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) { switch ($stationType) {
case 1: case 1:
$baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/mqtt?payload="; $baseUrl = "http://" . $this->ReadPropertyString("IP_Adresse") . "/mqtt?payload=";
@@ -772,17 +903,9 @@ class Ladestation_v2 extends IPSModule
case 4: case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; return;
case 5:
// Keine base Url nötig
break;
case 6:
// Keine base Url nötig
break;
} }
$ch = curl_init(); $ch = curl_init();
IPS_LogMessage("value", "nach konvertierung" . $value);
if ($value == 0) { if ($value == 0) {
switch ($stationType) { switch ($stationType) {
case 1: case 1:
@@ -801,42 +924,6 @@ class Ladestation_v2 extends IPSModule
case 4: case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; 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: default:
return "Invalid station type."; return "Invalid station type.";
} }
@@ -864,42 +951,6 @@ class Ladestation_v2 extends IPSModule
case 4: case 4:
// Nichts zu tun für Dummy station // Nichts zu tun für Dummy station
return; 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: default:
return "Invalid station type."; return "Invalid station type.";
} }
@@ -927,41 +978,206 @@ class Ladestation_v2 extends IPSModule
} }
} }
public function Refresh_Token(){ public function Refresh_Token()
{
$payload = json_encode([ // Easee-Zugangsdaten und Token werden ausschliesslich
'userName' => $this->ReadPropertyString("Username"), // im SignalR-Gateway verwaltet.
'password' => $this->ReadPropertyString("Password"), return;
]);
// 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 private function UsesEaseeGateway(int $stationType): bool
curl_close($ch); {
$this->SetBuffer("Token_Intern", (json_decode($response, true)['accessToken'])); return $stationType === 5 || $stationType === 6;
}
private function IsEaseeGatewayInstance(int $instanceID): bool
{
if ($instanceID <= 0 || !IPS_InstanceExists($instanceID)) {
return false;
}
$instance = IPS_GetInstance($instanceID);
return
$instance["ModuleInfo"]["ModuleID"] ===
self::EASEE_GATEWAY_MODULE_ID;
}
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);
$parentID = (int)$instance["ConnectionID"];
if (!$this->IsEaseeGatewayInstance($parentID)) {
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)) {
return;
}
if (!isset($response["state"]) || !is_array($response["state"])) {
return;
}
$updated = (int)($response["state"]["updated"] ?? time());
foreach (
[47, 104, 109, 110, 120, 182, 183, 184, 185]
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 (
!in_array(
$observationID,
[47, 104, 109, 110, 120, 182, 183, 184, 185],
true
)
) {
return;
}
$state = json_decode(
$this->GetBuffer("EaseeGatewayState"),
true
);
if (!is_array($state)) {
$state = [];
}
if ($observationID === 109) {
$previousMode = (int)($state["109"] ?? -1);
if ((int)$value === 1 || $previousMode === 1) {
foreach (
[110, 182, 183, 184, 185]
as $sessionObservationID
) {
$state[(string)$sessionObservationID] = 0;
}
}
}
$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"];
$currentState =
$chargerOpMode != 1 &&
($chargerOpMode != 6 || $this->GetValue("Car_detected")) &&
$chargerOpMode != 7;
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" => "SetDynamicChargerCurrent",
"serialNumber" => strtoupper(
trim($this->ReadPropertyString("Seriennummer"))
),
"amps" => $value,
]);
if (!is_array($response) || !($response["success"] ?? false)) {
return is_array($response)
? (string)(
$response["error"] ??
"Unbekannter Gateway-Fehler"
)
: "Easee Gateway nicht erreichbar";
}
return $response["body"] ?? null;
} }
public function CheckIdle($power) public function CheckIdle($power)
+2 -2
View File
@@ -12,12 +12,12 @@
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
"name": "CapacityTargetProperty", "name": "CapacityTargetProperty",
"caption": "Property-Name Kapazität" "caption": "Property-Name Kapazität (Batterie)"
}, },
{ {
"type": "SelectInstance", "type": "SelectInstance",
"name": "PowerTargetInstance", "name": "PowerTargetInstance",
"caption": "Instanz für Peakleistung" "caption": "Instanz für Peakleistung (Manager)"
}, },
{ {
"type": "ValidationTextBox", "type": "ValidationTextBox",
+1 -1
View File
@@ -6,7 +6,7 @@
"compatibility": { "compatibility": {
"version": "8.0" "version": "8.0"
}, },
"version": "2.001", "version": "2.1",
"build": 0, "build": 0,
"date": 0 "date": 0
} }