This commit is contained in:
@@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../libs/EaseeGatewayProtokoll.php';
|
||||
|
||||
use Belevo\EnelixEMS\EaseeGatewayProtokoll;
|
||||
|
||||
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 = '{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}';
|
||||
private const CHILD_EVENT_DATA_ID = '{107D5CFA-8F3D-4E38-8643-90DDFE6A3B4D}';
|
||||
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(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->RegisterPropertyBoolean('Active', true);
|
||||
$this->RegisterPropertyString('Username', '');
|
||||
$this->RegisterPropertyString('Password', '');
|
||||
$this->RegisterPropertyBoolean('VerifyCertificate', true);
|
||||
|
||||
// Nur fuer den automatisierten IP-Symcon-Funktionstest.
|
||||
$this->RegisterPropertyBoolean('Testmodus', false);
|
||||
|
||||
$this->RegisterAttributeString('ObservationCache', '{}');
|
||||
|
||||
$this->RegisterVariableBoolean('Connected', 'Easee verbunden', '~Switch', 10);
|
||||
$this->RegisterVariableInteger(
|
||||
'SubscriptionCount',
|
||||
'Angemeldete Ladestationen',
|
||||
'',
|
||||
20
|
||||
);
|
||||
$this->RegisterVariableString('LastError', 'Letzter Fehler', '', 30);
|
||||
|
||||
$this->RegisterTimer(
|
||||
'MaintainConnectionTimer',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'MaintainConnection', false);"
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'TokenRefreshTimer',
|
||||
0,
|
||||
"IPS_RequestAction(\$_IPS['TARGET'], 'RefreshToken', false);"
|
||||
);
|
||||
|
||||
$this->RequireParent(self::WEBSOCKET_MODULE_ID);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
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->setzeVerbunden(false);
|
||||
|
||||
if (!$this->ReadPropertyBoolean('Active')) {
|
||||
$this->SetStatus(104);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->SetStatus(102);
|
||||
$this->setzeLetztenFehler('');
|
||||
$this->setzeVerbunden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
trim($this->ReadPropertyString('Username')) === ''
|
||||
|| $this->ReadPropertyString('Password') === ''
|
||||
) {
|
||||
$this->SetStatus(201);
|
||||
$this->setzeLetztenFehler('Easee-Benutzername oder Passwort fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->stelleWebSocketParentSicher()) {
|
||||
$this->SetStatus(203);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetTimerInterval('MaintainConnectionTimer', 10000);
|
||||
$this->SetTimerInterval('TokenRefreshTimer', 1800000);
|
||||
if (!$this->erneuereZugangsdaten(false)) {
|
||||
$this->SetStatus(202);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'MaintainConnection':
|
||||
$this->pflegeVerbindung();
|
||||
return;
|
||||
|
||||
case 'RefreshToken':
|
||||
if ($this->erneuereZugangsdaten(false)) {
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'Reconnect':
|
||||
$this->SetBuffer('AccessToken', '');
|
||||
$this->SetBuffer('RefreshToken', '');
|
||||
if (
|
||||
$this->ReadPropertyBoolean('Testmodus')
|
||||
|| $this->erneuereZugangsdaten(false)
|
||||
) {
|
||||
$this->verbindeSignalR();
|
||||
}
|
||||
return;
|
||||
|
||||
case 'TestObservation':
|
||||
if (!$this->ReadPropertyBoolean('Testmodus') || !is_string($wert)) {
|
||||
throw new InvalidArgumentException('TestObservation ist nur im Testmodus zulaessig.');
|
||||
}
|
||||
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($daten)) {
|
||||
throw new InvalidArgumentException('TestObservation erwartet ein JSON-Objekt.');
|
||||
}
|
||||
$this->veroeffentlicheBeobachtung(
|
||||
EaseeGatewayProtokoll::seriennummer((string) ($daten['serialNumber'] ?? '')),
|
||||
(int) ($daten['id'] ?? 0),
|
||||
$daten['value'] ?? null
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
public function GetConfigurationForParent(): string
|
||||
{
|
||||
return json_encode([
|
||||
'Active' => $this->ReadPropertyBoolean('Active')
|
||||
&& !$this->ReadPropertyBoolean('Testmodus'),
|
||||
'URL' => $this->GetBuffer('WebSocketURL'),
|
||||
'VerifyCertificate' => $this->ReadPropertyBoolean('VerifyCertificate'),
|
||||
'Headers' => '[]',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function ReceiveData($jsonString): void
|
||||
{
|
||||
$paket = json_decode((string) $jsonString, true);
|
||||
if (!is_array($paket) || !isset($paket['Buffer'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->SetBuffer('LastReceive', (string) time());
|
||||
$puffer = $this->GetBuffer('ReceiveBuffer') . (string) $paket['Buffer'];
|
||||
$rahmen = explode(self::RECORD_SEPARATOR, $puffer);
|
||||
$this->SetBuffer('ReceiveBuffer', (string) array_pop($rahmen));
|
||||
|
||||
foreach ($rahmen as $eintrag) {
|
||||
if ($eintrag !== '') {
|
||||
$this->verarbeiteSignalRRahmen($eintrag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function ForwardData($jsonString): string
|
||||
{
|
||||
$paket = json_decode((string) $jsonString, true);
|
||||
if (!is_array($paket) || !isset($paket['Buffer'])) {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Ungueltiges Datenpaket.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
return $this->ProcessStationRequest((string) $paket['Buffer']);
|
||||
}
|
||||
|
||||
public function ProcessStationRequest($jsonString): string
|
||||
{
|
||||
$anfrage = json_decode((string) $jsonString, true);
|
||||
if (!is_array($anfrage) || !isset($anfrage['action'])) {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Ungueltige Gateway-Anfrage.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
$seriennummer = EaseeGatewayProtokoll::seriennummer(
|
||||
(string) ($anfrage['serialNumber'] ?? '')
|
||||
);
|
||||
|
||||
switch ($anfrage['action']) {
|
||||
case 'Subscribe':
|
||||
case 'GetState':
|
||||
if ($seriennummer === '') {
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Seriennummer fehlt.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
$this->registriereAbonnement($seriennummer);
|
||||
return $this->erstelleStatusantwort($seriennummer);
|
||||
|
||||
case 'SetDynamicChargerCurrent':
|
||||
return json_encode($this->setzeDynamischenLadestrom(
|
||||
$seriennummer,
|
||||
(float) ($anfrage['amps'] ?? -1)
|
||||
), JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
return json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Unbekannte Gateway-Aktion.',
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
private function stelleWebSocketParentSicher(): bool
|
||||
{
|
||||
$instanz = IPS_GetInstance($this->InstanceID);
|
||||
$parentID = (int) $instanz['ConnectionID'];
|
||||
|
||||
if ($parentID <= 0 && !$this->RequireParent(self::WEBSOCKET_MODULE_ID)) {
|
||||
$this->setzeLetztenFehler('WebSocket-Client konnte nicht erstellt werden.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$instanz = IPS_GetInstance($this->InstanceID);
|
||||
$parentID = (int) $instanz['ConnectionID'];
|
||||
if ($parentID <= 0 || !IPS_InstanceExists($parentID)) {
|
||||
$this->setzeLetztenFehler('WebSocket-Client ist nicht verbunden.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$parent = IPS_GetInstance($parentID);
|
||||
if ($parent['ModuleInfo']['ModuleID'] !== self::WEBSOCKET_MODULE_ID) {
|
||||
$this->setzeLetztenFehler('Ungueltige WebSocket-Schnittstelle.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function pflegeVerbindung(): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('Active') || $this->ReadPropertyBoolean('Testmodus')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$jetzt = time();
|
||||
$letzteAushandlung = (int) $this->GetBuffer('LastNegotiation');
|
||||
$letzterEmpfang = (int) $this->GetBuffer('LastReceive');
|
||||
$bereit = $this->GetBuffer('SignalRReady') === '1';
|
||||
|
||||
if ($bereit) {
|
||||
$this->sendeSignalRRahmen(['type' => 6]);
|
||||
if ($letzterEmpfang > 0 && ($jetzt - $letzterEmpfang) <= 90) {
|
||||
return;
|
||||
}
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->setzeVerbunden(false);
|
||||
}
|
||||
|
||||
if (($jetzt - $letzteAushandlung) >= 45) {
|
||||
$this->verbindeSignalR();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendeHandshake();
|
||||
}
|
||||
|
||||
private function verbindeSignalR(): bool
|
||||
{
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->SetStatus(102);
|
||||
$this->setzeVerbunden(true);
|
||||
return true;
|
||||
}
|
||||
if (!$this->stelleAccessTokenSicher()) {
|
||||
$this->SetStatus(202);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->SetBuffer('LastNegotiation', (string) time());
|
||||
$token = $this->GetBuffer('AccessToken');
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
self::SIGNALR_BASE_URL . '/negotiate?negotiateVersion=1',
|
||||
'',
|
||||
$token
|
||||
);
|
||||
if ($antwort['httpCode'] === 401 && $this->erneuereZugangsdaten(false)) {
|
||||
$token = $this->GetBuffer('AccessToken');
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
self::SIGNALR_BASE_URL . '/negotiate?negotiateVersion=1',
|
||||
'',
|
||||
$token
|
||||
);
|
||||
}
|
||||
if (!$antwort['success']) {
|
||||
$this->SetStatus(203);
|
||||
$this->setzeLetztenFehler(
|
||||
'Easee-Ereignisverbindung fehlgeschlagen: HTTP '
|
||||
. $antwort['httpCode']
|
||||
. ($antwort['error'] !== '' ? ' / ' . $antwort['error'] : '')
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$daten = json_decode($antwort['body'], true);
|
||||
$verbindungstoken = is_array($daten)
|
||||
? (string) ($daten['connectionToken'] ?? $daten['connectionId'] ?? '')
|
||||
: '';
|
||||
if ($verbindungstoken === '') {
|
||||
$this->SetStatus(203);
|
||||
$this->setzeLetztenFehler('Easee liefert kein Verbindungstoken.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = 'wss://streams.easee.com/hubs/chargers?id='
|
||||
. rawurlencode($verbindungstoken)
|
||||
. '&access_token=' . rawurlencode($token);
|
||||
$this->SetBuffer('WebSocketURL', $url);
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->SetBuffer('ReceiveBuffer', '');
|
||||
$this->SetBuffer('SubscribedThisConnection', '{}');
|
||||
$this->SetBuffer('PendingInvocations', '{}');
|
||||
$this->WriteAttributeString('ObservationCache', '{}');
|
||||
$this->setzeVerbunden(false);
|
||||
|
||||
if (!$this->konfiguriereWebSocketParent($url)) {
|
||||
$this->SetStatus(203);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->sendeHandshake();
|
||||
return true;
|
||||
}
|
||||
|
||||
private function konfiguriereWebSocketParent(string $url): bool
|
||||
{
|
||||
$parentID = (int) IPS_GetInstance($this->InstanceID)['ConnectionID'];
|
||||
if ($parentID <= 0) {
|
||||
$this->setzeLetztenFehler('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 sendeHandshake(): void
|
||||
{
|
||||
$this->sendeRohdaten(
|
||||
json_encode(['protocol' => 'json', 'version' => 1], JSON_THROW_ON_ERROR)
|
||||
. self::RECORD_SEPARATOR
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $rahmen */
|
||||
private function sendeSignalRRahmen(array $rahmen): void
|
||||
{
|
||||
$this->sendeRohdaten(
|
||||
json_encode($rahmen, JSON_THROW_ON_ERROR) . self::RECORD_SEPARATOR
|
||||
);
|
||||
}
|
||||
|
||||
private function sendeRohdaten(string $nutzdaten): void
|
||||
{
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
return;
|
||||
}
|
||||
$this->SendDataToParent(json_encode([
|
||||
'DataID' => self::SIMPLE_TX_DATA_ID,
|
||||
'Buffer' => $nutzdaten,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function verarbeiteSignalRRahmen(string $rahmen): void
|
||||
{
|
||||
if ($rahmen === '{}') {
|
||||
$this->SetBuffer('SignalRReady', '1');
|
||||
$this->SetStatus(102);
|
||||
$this->setzeLetztenFehler('');
|
||||
$this->setzeVerbunden(true);
|
||||
$this->sendeAlleAbonnements();
|
||||
return;
|
||||
}
|
||||
|
||||
$nachricht = json_decode($rahmen, true);
|
||||
if (!is_array($nachricht)) {
|
||||
$this->setzeLetztenFehler('Ungueltige Easee-Ereignisnachricht.');
|
||||
return;
|
||||
}
|
||||
if (isset($nachricht['error'])) {
|
||||
$this->SetStatus(203);
|
||||
$this->setzeLetztenFehler('Easee-Ereignisfehler: ' . $nachricht['error']);
|
||||
return;
|
||||
}
|
||||
|
||||
$typ = (int) ($nachricht['type'] ?? 0);
|
||||
if ($typ === 6) {
|
||||
return;
|
||||
}
|
||||
if ($typ === 7) {
|
||||
$this->SetBuffer('SignalRReady', '0');
|
||||
$this->setzeVerbunden(false);
|
||||
$this->setzeLetztenFehler('Easee-Ereignisverbindung wurde beendet.');
|
||||
return;
|
||||
}
|
||||
|
||||
$seriennummer = '';
|
||||
if ($typ === 3 && isset($nachricht['invocationId'])) {
|
||||
$offen = $this->lesePufferArray('PendingInvocations');
|
||||
$aufrufID = (string) $nachricht['invocationId'];
|
||||
$seriennummer = (string) ($offen[$aufrufID] ?? '');
|
||||
unset($offen[$aufrufID]);
|
||||
$this->SetBuffer(
|
||||
'PendingInvocations',
|
||||
json_encode($offen, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
foreach (['arguments', 'result'] as $feld) {
|
||||
if (!isset($nachricht[$feld])) {
|
||||
continue;
|
||||
}
|
||||
$abonnements = array_keys($this->lesePufferArray('Subscriptions'));
|
||||
foreach (EaseeGatewayProtokoll::extrahiereBeobachtungen(
|
||||
$nachricht[$feld],
|
||||
$abonnements,
|
||||
$seriennummer
|
||||
) as $beobachtung) {
|
||||
$this->veroeffentlicheBeobachtung(
|
||||
$beobachtung['Seriennummer'],
|
||||
$beobachtung['ID'],
|
||||
$beobachtung['Wert']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function registriereAbonnement(string $seriennummer): void
|
||||
{
|
||||
$abonnements = $this->lesePufferArray('Subscriptions');
|
||||
if (!isset($abonnements[$seriennummer])) {
|
||||
$abonnements[$seriennummer] = true;
|
||||
$this->SetBuffer(
|
||||
'Subscriptions',
|
||||
json_encode($abonnements, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->SetValue('SubscriptionCount', count($abonnements));
|
||||
}
|
||||
|
||||
if ($this->GetBuffer('SignalRReady') === '1') {
|
||||
$this->sendeAbonnement($seriennummer);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeAlleAbonnements(): void
|
||||
{
|
||||
foreach (array_keys($this->lesePufferArray('Subscriptions')) as $seriennummer) {
|
||||
$this->sendeAbonnement((string) $seriennummer);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendeAbonnement(string $seriennummer): void
|
||||
{
|
||||
$gesendet = $this->lesePufferArray('SubscribedThisConnection');
|
||||
if (isset($gesendet[$seriennummer])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aufrufID = (string) (((int) $this->GetBuffer('InvocationID')) + 1);
|
||||
$this->SetBuffer('InvocationID', $aufrufID);
|
||||
$offen = $this->lesePufferArray('PendingInvocations');
|
||||
$offen[$aufrufID] = $seriennummer;
|
||||
$this->SetBuffer(
|
||||
'PendingInvocations',
|
||||
json_encode($offen, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->sendeSignalRRahmen([
|
||||
'type' => 1,
|
||||
'invocationId' => $aufrufID,
|
||||
'target' => 'SubscribeWithCurrentState',
|
||||
'arguments' => [$seriennummer, true],
|
||||
]);
|
||||
|
||||
$gesendet[$seriennummer] = true;
|
||||
$this->SetBuffer(
|
||||
'SubscribedThisConnection',
|
||||
json_encode($gesendet, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
private function veroeffentlicheBeobachtung(
|
||||
string $seriennummer,
|
||||
int $id,
|
||||
$wert
|
||||
): void {
|
||||
if (
|
||||
$seriennummer === ''
|
||||
|| !in_array($id, EaseeGatewayProtokoll::BEOBACHTUNGEN, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
if (!isset($cache[$seriennummer]) || !is_array($cache[$seriennummer])) {
|
||||
$cache[$seriennummer] = [];
|
||||
}
|
||||
if ($id === 109 && (int) $wert === 1) {
|
||||
foreach ([110, 120, 182, 183, 184, 185] as $sessionID) {
|
||||
$cache[$seriennummer][(string) $sessionID] = 0;
|
||||
}
|
||||
}
|
||||
$cache[$seriennummer][(string) $id] = $wert;
|
||||
$cache[$seriennummer]['updated'] = time();
|
||||
$this->WriteAttributeString(
|
||||
'ObservationCache',
|
||||
json_encode($cache, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
|
||||
$this->SendDataToChildren(json_encode([
|
||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
||||
'Buffer' => json_encode([
|
||||
'type' => 'Observation',
|
||||
'serialNumber' => $seriennummer,
|
||||
'id' => $id,
|
||||
'value' => $wert,
|
||||
'timestamp' => time(),
|
||||
], JSON_THROW_ON_ERROR),
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function erstelleStatusantwort(string $seriennummer): string
|
||||
{
|
||||
$cache = $this->leseAttributArray('ObservationCache');
|
||||
return json_encode([
|
||||
'success' => true,
|
||||
'connected' => $this->GetBuffer('SignalRReady') === '1'
|
||||
|| $this->ReadPropertyBoolean('Testmodus'),
|
||||
'state' => $cache[$seriennummer] ?? [],
|
||||
], JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function setzeDynamischenLadestrom(string $seriennummer, float $ampere): array
|
||||
{
|
||||
if ($seriennummer === '') {
|
||||
return ['success' => false, 'error' => 'Seriennummer fehlt.'];
|
||||
}
|
||||
if (
|
||||
!is_finite($ampere)
|
||||
|| floor($ampere) !== $ampere
|
||||
|| $ampere < 0
|
||||
|| $ampere > 32
|
||||
|| ($ampere > 0 && $ampere < 6)
|
||||
) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Strom muss 0 A oder eine ganze Zahl zwischen 6 und 32 A sein.',
|
||||
];
|
||||
}
|
||||
if ($this->ReadPropertyBoolean('Testmodus')) {
|
||||
$this->SetBuffer('LetzterTestbefehl', json_encode([
|
||||
'serialNumber' => $seriennummer,
|
||||
'amps' => $ampere,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
return ['success' => true, 'httpCode' => 200, 'body' => '{}'];
|
||||
}
|
||||
|
||||
return $this->autorisierteApiAnfrage(
|
||||
'POST',
|
||||
'/api/chargers/' . rawurlencode($seriennummer)
|
||||
. '/commands/set_dynamic_charger_current',
|
||||
json_encode([
|
||||
'amps' => (int) round($ampere),
|
||||
'minutes' => 0,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function autorisierteApiAnfrage(
|
||||
string $methode,
|
||||
string $pfad,
|
||||
string $inhalt
|
||||
): array {
|
||||
if (!$this->stelleAccessTokenSicher()) {
|
||||
return ['success' => false, 'error' => 'Kein Easee-Access-Token.'];
|
||||
}
|
||||
|
||||
$antwort = $this->httpAnfrage(
|
||||
$methode,
|
||||
self::API_BASE_URL . $pfad,
|
||||
$inhalt,
|
||||
$this->GetBuffer('AccessToken')
|
||||
);
|
||||
if ($antwort['httpCode'] === 401 && $this->erneuereZugangsdaten(false)) {
|
||||
$antwort = $this->httpAnfrage(
|
||||
$methode,
|
||||
self::API_BASE_URL . $pfad,
|
||||
$inhalt,
|
||||
$this->GetBuffer('AccessToken')
|
||||
);
|
||||
}
|
||||
if (!$antwort['success']) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $antwort['error'] !== ''
|
||||
? $antwort['error']
|
||||
: 'Easee-HTTP-Fehler ' . $antwort['httpCode'] . '.',
|
||||
'httpCode' => $antwort['httpCode'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'httpCode' => $antwort['httpCode'],
|
||||
'body' => $antwort['body'],
|
||||
];
|
||||
}
|
||||
|
||||
private function erneuereZugangsdaten(bool $neuVerbinden = true): bool
|
||||
{
|
||||
$tokenpaar = null;
|
||||
$refreshToken = $this->GetBuffer('RefreshToken');
|
||||
if ($refreshToken !== '') {
|
||||
$tokenpaar = $this->fordereTokenpaarAn(
|
||||
self::API_BASE_URL . '/api/accounts/refresh_token',
|
||||
['refreshToken' => $refreshToken]
|
||||
);
|
||||
}
|
||||
if ($tokenpaar === null) {
|
||||
$tokenpaar = $this->fordereTokenpaarAn(
|
||||
self::API_BASE_URL . '/api/accounts/login',
|
||||
[
|
||||
'userName' => $this->ReadPropertyString('Username'),
|
||||
'password' => $this->ReadPropertyString('Password'),
|
||||
]
|
||||
);
|
||||
}
|
||||
if ($tokenpaar === null || !isset($tokenpaar['accessToken'])) {
|
||||
$this->setzeLetztenFehler('Easee-Anmeldung oder Token-Erneuerung fehlgeschlagen.');
|
||||
$this->SetStatus(202);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->SetBuffer('AccessToken', (string) $tokenpaar['accessToken']);
|
||||
if (isset($tokenpaar['refreshToken'])) {
|
||||
$this->SetBuffer('RefreshToken', (string) $tokenpaar['refreshToken']);
|
||||
}
|
||||
if ($neuVerbinden) {
|
||||
return $this->verbindeSignalR();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function stelleAccessTokenSicher(): bool
|
||||
{
|
||||
return $this->GetBuffer('AccessToken') !== ''
|
||||
|| $this->erneuereZugangsdaten(false);
|
||||
}
|
||||
|
||||
/** @param array<string, string> $nutzdaten
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function fordereTokenpaarAn(string $url, array $nutzdaten): ?array
|
||||
{
|
||||
$antwort = $this->httpAnfrage(
|
||||
'POST',
|
||||
$url,
|
||||
json_encode($nutzdaten, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
if (!$antwort['success']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$daten = json_decode($antwort['body'], true);
|
||||
return is_array($daten) ? $daten : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success: bool, body: string, error: string, httpCode: int}
|
||||
*/
|
||||
private function httpAnfrage(
|
||||
string $methode,
|
||||
string $url,
|
||||
string $inhalt = '',
|
||||
string $bearerToken = ''
|
||||
): array {
|
||||
$kopf = ['Accept: application/json', 'Content-Type: application/json'];
|
||||
if ($bearerToken !== '') {
|
||||
$kopf[] = 'Authorization: Bearer ' . $bearerToken;
|
||||
}
|
||||
|
||||
$curl = curl_init($url);
|
||||
if ($curl === false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'body' => '',
|
||||
'error' => 'HTTP-Anfrage konnte nicht initialisiert werden.',
|
||||
'httpCode' => 0,
|
||||
];
|
||||
}
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => $methode,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_HTTPHEADER => $kopf,
|
||||
CURLOPT_POSTFIELDS => $inhalt,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->ReadPropertyBoolean('VerifyCertificate'),
|
||||
CURLOPT_SSL_VERIFYHOST => $this->ReadPropertyBoolean('VerifyCertificate') ? 2 : 0,
|
||||
]);
|
||||
|
||||
$antwort = curl_exec($curl);
|
||||
$fehler = curl_error($curl);
|
||||
$httpStatus = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
|
||||
return [
|
||||
'success' => $antwort !== false
|
||||
&& $fehler === ''
|
||||
&& $httpStatus >= 200
|
||||
&& $httpStatus < 300,
|
||||
'body' => $antwort === false ? '' : (string) $antwort,
|
||||
'error' => $fehler,
|
||||
'httpCode' => $httpStatus,
|
||||
];
|
||||
}
|
||||
|
||||
private function setzeVerbunden(bool $verbunden): void
|
||||
{
|
||||
$this->SetValue('Connected', $verbunden);
|
||||
$this->SendDataToChildren(json_encode([
|
||||
'DataID' => self::CHILD_EVENT_DATA_ID,
|
||||
'Buffer' => json_encode([
|
||||
'type' => 'GatewayStatus',
|
||||
'connected' => $verbunden,
|
||||
], JSON_THROW_ON_ERROR),
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
private function setzeLetztenFehler(string $nachricht): void
|
||||
{
|
||||
$this->SetValue('LastError', $nachricht);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function leseAttributArray(string $name): array
|
||||
{
|
||||
$wert = json_decode($this->ReadAttributeString($name), true);
|
||||
return is_array($wert) ? $wert : [];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function lesePufferArray(string $name): array
|
||||
{
|
||||
$wert = json_decode($this->GetBuffer($name), true);
|
||||
return is_array($wert) ? $wert : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user