Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21650e07cf | ||
|
|
17946b3a02 | ||
|
|
20324509b1 | ||
|
|
6b8db1acec | ||
|
|
6edd688c1f | ||
|
|
ab9f65b5b1 | ||
|
|
1526b48ef2 | ||
|
|
21d14d52ed | ||
|
|
7c1b992729 | ||
|
|
4491e244fb | ||
|
|
c46bcd0488 | ||
|
|
b4fade6fe7 | ||
|
|
f4ee0ea78a | ||
|
|
125cc14c94 | ||
|
|
7959e7e1ae | ||
|
|
3d4c30ae02 | ||
|
|
d254eaa60f | ||
|
|
6dd38afdc2 | ||
|
|
d19fe26299 | ||
|
|
9da468f89f | ||
|
|
c9ca9661cf | ||
|
|
a87461eefa | ||
|
|
a9d3712ef9 | ||
|
|
2ca9503c46 | ||
|
|
4d136315c0 | ||
|
|
db5d53820b | ||
|
|
6763aec78b | ||
|
|
87ebda1e4f | ||
|
|
97ce906c91 | ||
|
|
d37fa96a31 | ||
|
|
d8d3e2c998 | ||
|
|
43623376e4 | ||
|
|
4e3c6c1b13 | ||
|
|
69da32646a | ||
|
|
875a961afb | ||
|
|
89f348123f | ||
|
|
6354fac434 | ||
|
|
9200e2bc51 | ||
|
|
650ea80e2b | ||
|
|
313c42d925 | ||
|
|
f3393a84dc | ||
|
|
77712ee4fd | ||
|
|
6ce598542f | ||
|
|
fefd1fe2f6 | ||
|
|
a70aad8d45 | ||
|
|
ec42f28e50 | ||
|
|
a459f66847 | ||
|
|
dc38b89bf6 |
@@ -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
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "Active",
|
||||||
|
"caption": "Gateway aktiv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Username",
|
||||||
|
"caption": "Easee Benutzername"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PasswordTextBox",
|
||||||
|
"name": "Password",
|
||||||
|
"caption": "Easee Passwort"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CheckBox",
|
||||||
|
"name": "VerifyCertificate",
|
||||||
|
"caption": "TLS-Zertifikat prüfen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"caption": "Ein Gateway wird von allen Easee-Ladestationen desselben Kontos gemeinsam verwendet."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"caption": "Verbindung neu aufbauen",
|
||||||
|
"onClick": "IPS_RequestAction($id, \"Reconnect\", 0);"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"status": [
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Easee-Zugangsdaten fehlen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 202,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "Easee-Anmeldung fehlgeschlagen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": 203,
|
||||||
|
"icon": "error",
|
||||||
|
"caption": "SignalR-Verbindung fehlgeschlagen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"id": "{7A1F1D4B-3E7D-4A3B-9E54-61C8C3B7F201}",
|
||||||
|
"name": "EaseeGateway",
|
||||||
|
"type": 2,
|
||||||
|
"vendor": "Belevo AG",
|
||||||
|
"aliases": [
|
||||||
|
"Easee SignalR Gateway"
|
||||||
|
],
|
||||||
|
"parentRequirements": [
|
||||||
|
"{79827379-F36E-4ADA-8A95-5F8D1DC92FA9}"
|
||||||
|
],
|
||||||
|
"childRequirements": [
|
||||||
|
"{A8EF2E37-6B35-4E92-A7D1-4F8172790BA2}"
|
||||||
|
],
|
||||||
|
"implemented": [
|
||||||
|
"{018EF6B5-AB94-40C6-AA53-46943E824ACF}",
|
||||||
|
"{BC4E9B83-9C9B-44C3-A902-5CEB45B0E5B1}"
|
||||||
|
],
|
||||||
|
"prefix": "EASEEGW",
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,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
@@ -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);`
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user