856 lines
28 KiB
PHP
856 lines
28 KiB
PHP
<?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;
|
|
}
|
|
|
|
$this->speichereBeobachtungImCache($seriennummer, $id, $wert);
|
|
|
|
$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 speichereBeobachtungImCache(
|
|
string $seriennummer,
|
|
int $id,
|
|
$wert
|
|
): void {
|
|
$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)
|
|
);
|
|
}
|
|
|
|
private function stelleAktuellenStatusSicher(string $seriennummer): void
|
|
{
|
|
$cache = $this->leseAttributArray('ObservationCache');
|
|
$zustand = $cache[$seriennummer] ?? [];
|
|
if (
|
|
(is_array($zustand) && array_key_exists('109', $zustand))
|
|
|| $this->ReadPropertyBoolean('Testmodus')
|
|
|| $this->GetBuffer('SignalRReady') !== '1'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$antwort = $this->autorisierteApiAnfrage(
|
|
'GET',
|
|
'/state/' . rawurlencode($seriennummer) . '/observations?ids='
|
|
. rawurlencode(implode(',', EaseeGatewayProtokoll::BEOBACHTUNGEN)),
|
|
''
|
|
);
|
|
if (!($antwort['success'] ?? false)) {
|
|
$this->setzeLetztenFehler(
|
|
'Easee-Status konnte nicht geladen werden: '
|
|
. (string) ($antwort['error'] ?? 'Unbekannter API-Fehler.')
|
|
);
|
|
return;
|
|
}
|
|
|
|
$daten = json_decode((string) ($antwort['body'] ?? ''), true);
|
|
if (!is_array($daten)) {
|
|
$this->setzeLetztenFehler('Easee-Statusantwort ist ungueltig.');
|
|
return;
|
|
}
|
|
$beobachtungen = EaseeGatewayProtokoll::extrahiereBeobachtungen(
|
|
$daten,
|
|
[$seriennummer],
|
|
$seriennummer
|
|
);
|
|
$betriebsstatusGeladen = false;
|
|
foreach ($beobachtungen as $beobachtung) {
|
|
$this->speichereBeobachtungImCache(
|
|
$beobachtung['Seriennummer'],
|
|
$beobachtung['ID'],
|
|
$beobachtung['Wert']
|
|
);
|
|
$betriebsstatusGeladen = $betriebsstatusGeladen
|
|
|| $beobachtung['ID'] === 109;
|
|
}
|
|
if (!$betriebsstatusGeladen) {
|
|
$this->setzeLetztenFehler(
|
|
'Easee-Statusantwort enthaelt keinen Betriebsstatus (109).'
|
|
);
|
|
return;
|
|
}
|
|
$this->setzeLetztenFehler('');
|
|
}
|
|
|
|
private function erstelleStatusantwort(string $seriennummer): string
|
|
{
|
|
$this->stelleAktuellenStatusSicher($seriennummer);
|
|
$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 : [];
|
|
}
|
|
}
|