230 lines
7.8 KiB
PHP
230 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixEMS;
|
|
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
final class LadestationAdapter
|
|
{
|
|
public const GO_E_ALT = 1;
|
|
public const GO_E_GEMINI = 2;
|
|
public const SMART_ME_PICO = 3;
|
|
|
|
/**
|
|
* @param array<string, string> $konfiguration
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function statusAnfrage(int $geraetetyp, array $konfiguration): array
|
|
{
|
|
if ($geraetetyp === self::GO_E_ALT) {
|
|
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=');
|
|
}
|
|
if ($geraetetyp === self::GO_E_GEMINI) {
|
|
return self::anfrage('GET', self::lokaleBasisUrl($konfiguration) . '/api/status');
|
|
}
|
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
|
$id = self::pflichtwert($konfiguration, 'GeraeteID');
|
|
|
|
return self::anfrage(
|
|
'GET',
|
|
'https://api.smart-me.com/pico/charging/' . rawurlencode($id),
|
|
$konfiguration
|
|
);
|
|
}
|
|
|
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $konfiguration
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function steuerAnfragen(
|
|
int $geraetetyp,
|
|
array $konfiguration,
|
|
int $ladestrom
|
|
): array {
|
|
if ($ladestrom < 0 || $ladestrom > 32 || ($ladestrom > 0 && $ladestrom < 6)) {
|
|
throw new InvalidArgumentException('Ladestrom muss 0 oder 6 bis 32 A betragen.');
|
|
}
|
|
|
|
if ($geraetetyp === self::GO_E_ALT) {
|
|
$basis = self::lokaleBasisUrl($konfiguration) . '/mqtt?payload=';
|
|
if ($ladestrom === 0) {
|
|
return [self::anfrage('GET', $basis . 'alw=0')];
|
|
}
|
|
|
|
return [
|
|
self::anfrage('GET', $basis . 'alw=1'),
|
|
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
|
];
|
|
}
|
|
|
|
if ($geraetetyp === self::GO_E_GEMINI) {
|
|
$basis = self::lokaleBasisUrl($konfiguration) . '/api/set?';
|
|
if ($ladestrom === 0) {
|
|
return [self::anfrage('GET', $basis . 'frc=1&sua=false')];
|
|
}
|
|
|
|
return [
|
|
self::anfrage('GET', $basis . 'frc=2'),
|
|
self::anfrage('GET', $basis . 'amp=' . $ladestrom),
|
|
];
|
|
}
|
|
|
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
|
$seriennummer = self::pflichtwert($konfiguration, 'Seriennummer');
|
|
|
|
return [self::anfrage(
|
|
'POST',
|
|
'https://api.smart-me.com/pico/loadmanagementgroup/current/'
|
|
. rawurlencode($seriennummer)
|
|
. '?current=' . ($ladestrom * 1000),
|
|
$konfiguration
|
|
)];
|
|
}
|
|
|
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
|
}
|
|
|
|
/**
|
|
* @return array{Rohstatus: int, Istleistung_W: float, Iststrom_A?: float, Phasenzahl?: int}
|
|
*/
|
|
public static function dekodiereStatus(int $geraetetyp, string $antwort): array
|
|
{
|
|
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!is_array($daten)) {
|
|
throw new RuntimeException('Die Geraeteantwort ist kein JSON-Objekt.');
|
|
}
|
|
|
|
if ($geraetetyp === self::GO_E_ALT || $geraetetyp === self::GO_E_GEMINI) {
|
|
if (!isset($daten['car'], $daten['nrg'])
|
|
|| !is_numeric($daten['car'])
|
|
|| !is_array($daten['nrg'])
|
|
|| !isset($daten['nrg'][11])
|
|
|| !is_numeric($daten['nrg'][11])
|
|
) {
|
|
throw new RuntimeException('Die go-e-Antwort enthaelt nicht alle Statusfelder.');
|
|
}
|
|
|
|
$leistungsfaktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
|
|
$status = [
|
|
'Rohstatus' => (int) $daten['car'],
|
|
'Istleistung_W' => (float) $daten['nrg'][11] * $leistungsfaktor,
|
|
];
|
|
if (isset($daten['nrg'][4], $daten['nrg'][5], $daten['nrg'][6])
|
|
&& is_numeric($daten['nrg'][4])
|
|
&& is_numeric($daten['nrg'][5])
|
|
&& is_numeric($daten['nrg'][6])
|
|
) {
|
|
$stromfaktor = $geraetetyp === self::GO_E_ALT ? 0.1 : 1.0;
|
|
$phasenstroeme = [
|
|
(float) $daten['nrg'][4] * $stromfaktor,
|
|
(float) $daten['nrg'][5] * $stromfaktor,
|
|
(float) $daten['nrg'][6] * $stromfaktor,
|
|
];
|
|
$status['Iststrom_A'] = max($phasenstroeme);
|
|
$aktivePhasen = count(array_filter(
|
|
$phasenstroeme,
|
|
static fn (float $strom): bool => $strom >= 1.0
|
|
));
|
|
if ($aktivePhasen > 0) {
|
|
$status['Phasenzahl'] = $aktivePhasen >= 2 ? 3 : 1;
|
|
}
|
|
}
|
|
|
|
return $status;
|
|
}
|
|
|
|
if ($geraetetyp === self::SMART_ME_PICO) {
|
|
if (!isset($daten['State'], $daten['ActiveChargingPower'])
|
|
|| !is_numeric($daten['State'])
|
|
|| !is_numeric($daten['ActiveChargingPower'])
|
|
) {
|
|
throw new RuntimeException('Die Pico-Antwort enthaelt nicht alle Statusfelder.');
|
|
}
|
|
|
|
return [
|
|
'Rohstatus' => (int) $daten['State'],
|
|
'Istleistung_W' => round((float) $daten['ActiveChargingPower'] * 1000.0),
|
|
];
|
|
}
|
|
|
|
throw new InvalidArgumentException('Unbekannter Geraetetyp.');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $konfiguration
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function anfrage(
|
|
string $methode,
|
|
string $url,
|
|
array $konfiguration = []
|
|
): array {
|
|
$anfrage = ['Methode' => $methode, 'URL' => $url];
|
|
if (str_starts_with($url, 'https://api.smart-me.com/')) {
|
|
$anfrage['Benutzername'] = self::pflichtwert($konfiguration, 'Benutzername');
|
|
$anfrage['Passwort'] = self::pflichtwert($konfiguration, 'Passwort');
|
|
}
|
|
|
|
return $anfrage;
|
|
}
|
|
|
|
/** @param array<string, string> $konfiguration */
|
|
private static function lokaleBasisUrl(array $konfiguration): string
|
|
{
|
|
$adresse = trim(self::pflichtwert($konfiguration, 'Geraeteadresse'));
|
|
if (str_contains($adresse, '/') || str_contains($adresse, '?')) {
|
|
throw new InvalidArgumentException('Geraeteadresse muss ein Hostname oder eine IP-Adresse sein.');
|
|
}
|
|
|
|
return 'http://' . $adresse;
|
|
}
|
|
|
|
/** @param array<string, string> $konfiguration */
|
|
private static function pflichtwert(array $konfiguration, string $name): string
|
|
{
|
|
$wert = trim($konfiguration[$name] ?? '');
|
|
if ($wert === '') {
|
|
throw new InvalidArgumentException($name . ' darf nicht leer sein.');
|
|
}
|
|
|
|
return $wert;
|
|
}
|
|
}
|
|
|
|
final class LadestationTransport
|
|
{
|
|
/** @var callable(array<string, mixed>): string */
|
|
private $anfrageAusfuehren;
|
|
|
|
/** @param callable(array<string, mixed>): string $anfrageAusfuehren */
|
|
public function __construct(callable $anfrageAusfuehren)
|
|
{
|
|
$this->anfrageAusfuehren = $anfrageAusfuehren;
|
|
}
|
|
|
|
/** @param array<string, string> $konfiguration */
|
|
public function statusLesen(int $geraetetyp, array $konfiguration): string
|
|
{
|
|
return ($this->anfrageAusfuehren)(
|
|
LadestationAdapter::statusAnfrage($geraetetyp, $konfiguration)
|
|
);
|
|
}
|
|
|
|
/** @param array<string, string> $konfiguration */
|
|
public function ladestromSetzen(
|
|
int $geraetetyp,
|
|
array $konfiguration,
|
|
int $ladestrom
|
|
): void {
|
|
foreach (LadestationAdapter::steuerAnfragen($geraetetyp, $konfiguration, $ladestrom) as $anfrage) {
|
|
($this->anfrageAusfuehren)($anfrage);
|
|
}
|
|
}
|
|
}
|