Files
Enelix-EMS/libs/LadestationAdapter.php
T
2026-09-21 18:46:17 +00:00

209 lines
6.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}
*/
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.');
}
$faktor = $geraetetyp === self::GO_E_ALT ? 10.0 : 1.0;
return [
'Rohstatus' => (int) $daten['car'],
'Istleistung_W' => (float) $daten['nrg'][11] * $faktor,
];
}
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);
}
}
}