Files
Enelix-EMS/LadestationGateway/module.php
T
dh ccecc437e9
Tests / test (push) Successful in 45s
fix(easee): wartende Ladevorgaenge starten
2026-09-29 09:34:37 +00:00

1123 lines
41 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../libs/VerbraucherSchnittstelle.php';
require_once __DIR__ . '/../libs/VerbraucherBasisTrait.php';
require_once __DIR__ . '/../libs/Nachrichtenvertrag.php';
require_once __DIR__ . '/../libs/LadestationRegler.php';
require_once __DIR__ . '/../libs/EaseeLadestatus.php';
require_once __DIR__ . '/../libs/EaseeGatewayProtokoll.php';
use Belevo\EnelixEMS\EaseeGatewayProtokoll;
use Belevo\EnelixEMS\EaseeLadestatus;
use Belevo\EnelixEMS\LadestationRegler;
use Belevo\EnelixEMS\Nachrichtenvertrag;
use Belevo\EnelixEMS\VerbraucherBasisTrait;
use Belevo\EnelixEMS\VerbraucherSchnittstelle;
class LadestationGateway extends IPSModule implements VerbraucherSchnittstelle
{
use VerbraucherBasisTrait;
private const MANAGER_MODULE_ID = '{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}';
private const EASEE_GATEWAY_MODULE_ID = '{B7552FC9-87BC-49D0-8CCB-076BBB045BBE}';
private const GATEWAY_REQUEST_DATA_ID = '{7AEF3DF7-DA5B-47C5-BCDC-0110D06DDC04}';
private const STATUS_AKTIV = 102;
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
private const STATUS_GATEWAY_GETRENNT = 202;
private const STATUS_WARTET_AUF_DATEN = 203;
private const STATUS_LADEFEHLER = 204;
/** @var list<string> */
private const DIAGNOSE_VARIABLEN = [
'Istleistung',
'Leistungsquelle',
'Sollleistung',
'SollwertGueltig',
'Verfuegbar',
'AenderungMoeglich',
'Stoerung',
'Stoertext',
'GatewayVerbunden',
'ApiMaximalstrom',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose',
];
public function Create(): void
{
parent::Create();
$this->registriereVerbraucherBasis();
$this->RegisterPropertyInteger('Betriebsmodus', EaseeLadestatus::BETRIEBSMODUS_EASEE);
$this->RegisterPropertyString('Ladestationskennung', '');
$this->RegisterPropertyInteger('MaximalerLadestrom', 16);
$this->RegisterPropertyBoolean('Ladefreigabe', true);
$this->RegisterPropertyBoolean('Solarladen', true);
$this->RegisterPropertyBoolean('DiagnosevariablenAnzeigen', false);
$this->RegisterVariableBoolean(
'FahrzeugVerbunden',
'Fahrzeug verbunden',
'~Switch',
100
);
$this->RegisterVariableBoolean(
'FahrzeugGeladen',
'Fahrzeug geladen',
'~Switch',
110
);
$this->RegisterVariableInteger('Fahrzeugstatus', 'Fahrzeugstatus', '', 120);
$this->RegisterVariableFloat('Ladestrom', 'Ladestrom', '', 130);
$this->RegisterVariableInteger('Phasenzahl', 'Phasenzahl', '', 140);
$this->RegisterAttributeBoolean('InitialwerteGesetzt', false);
$this->RegisterAttributeBoolean('KonfigurationLadefreigabe', false);
$this->RegisterAttributeBoolean('KonfigurationSolarladen', true);
$this->RegisterAttributeBoolean('ZustandLadefreigabe', false);
$this->RegisterAttributeBoolean('ZustandSolarladen', true);
$this->RegisterAttributeBoolean('GatewayVerbunden', false);
$this->RegisterAttributeString('EaseeBeobachtungen', '{}');
$this->RegisterAttributeInteger('LetzteBeobachtung', 0);
$this->RegisterAttributeInteger('ApiMaximalstrom', 16);
$this->RegisterAttributeBoolean('ZustandEaseeAktiviert', true);
$this->RegisterAttributeBoolean('ZustandAutorisierungErforderlich', false);
$this->RegisterAttributeBoolean('ZustandRemoteStartErforderlich', false);
$this->RegisterAttributeBoolean('ZustandSmartCharging', false);
$this->RegisterAttributeInteger('ZustandGrundKeinLadestrom', 0);
$this->RegisterAttributeBoolean('ZustandFahrzeugVerbunden', false);
$this->RegisterAttributeBoolean('ZustandFahrzeugGeladen', false);
$this->RegisterAttributeInteger('ZustandFahrzeugstatus', 0);
$this->RegisterAttributeFloat('ZustandLadestrom', 0.0);
$this->RegisterAttributeInteger('ZustandPhasenzahl', 0);
$this->RegisterAttributeFloat('ZustandIstleistung', 0.0);
$this->RegisterAttributeInteger('ZustandSollleistung', 0);
$this->RegisterAttributeBoolean('ZustandSollwertGueltig', false);
$this->RegisterAttributeBoolean('ZustandVerfuegbar', false);
$this->RegisterAttributeBoolean('ZustandAenderungMoeglich', false);
$this->RegisterAttributeBoolean('ZustandStoerung', false);
$this->RegisterAttributeString('ZustandStoertext', '');
$this->RegisterAttributeString('Leistungsangebot', '[0]');
$this->RegisterAttributeString('Betriebsart', Nachrichtenvertrag::BETRIEBSART_PV);
$this->RegisterAttributeInteger('LetzteManagerID', 0);
$this->RegisterAttributeInteger('LetzteVorgabeZeit', 0);
$this->RegisterAttributeInteger('LetzterGesetzterStrom', -1);
$this->RegisterAttributeBoolean('LadestartFuerVerbindungGesendet', false);
$this->RegisterAttributeString('LetzterGeraetebefehl', '');
$this->RegisterAttributeString('LetzterStartbefehl', '');
$this->RegisterTimer(
'Meldezyklus',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', false);"
);
$this->RegisterTimer(
'RueckmeldungVerzoegert',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'Melden', true);"
);
$this->RegisterTimer(
'VorgabeTimeout',
0,
"IPS_RequestAction(\$_IPS['TARGET'], 'VorgabeTimeout', false);"
);
$this->ConnectParent(self::EASEE_GATEWAY_MODULE_ID);
}
public function ApplyChanges(): void
{
parent::ApplyChanges();
$this->initialisiereLokaleEinstellungen();
$this->aktualisiereVariablen();
$this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
try {
$this->pruefeKonfiguration();
} catch (Throwable $fehler) {
$this->deaktiviereTimer();
$this->setzeZustand('Verfuegbar', false);
$this->setzeZustand('AenderungMoeglich', false);
$this->setzeStoerung($fehler->getMessage());
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
$this->protokolliere('Konfiguration', $fehler->getMessage());
return;
}
$this->SetTimerInterval(
'Meldezyklus',
$this->ReadPropertyInteger('Meldeintervall') * 1000
);
$this->statusAnfordern();
$this->aktualisiere(true);
}
public function RequestAction($ident, $wert): void
{
switch ($ident) {
case 'Aktiv':
$this->SetValue('Aktiv', (bool) $wert);
if (!(bool) $wert) {
$this->setzeZustand('SollwertGueltig', false);
}
$this->aktualisiere(true);
return;
case 'Ladefreigabe':
$this->WriteAttributeBoolean('ZustandLadefreigabe', (bool) $wert);
$this->setzeSichtbareVariable('Ladefreigabe', (bool) $wert);
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(true);
return;
case 'Solarladen':
$solarladen = EaseeLadestatus::solarladenErzwungen(
$this->ReadPropertyInteger('Betriebsmodus')
) ? true : (bool) $wert;
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
$this->setzeSichtbareVariable('Solarladen', $solarladen);
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(true);
return;
case 'StatusAnfordern':
$this->statusAnfordern();
return;
case 'VorgabeTimeout':
$this->SetTimerInterval('VorgabeTimeout', 0);
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(true);
return;
case 'Melden':
if ((bool) $wert) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 0);
}
$this->sendeVerbraucherdaten();
return;
case 'ManagerdatenEmpfangen':
if (!is_string($wert)) {
throw new InvalidArgumentException(
'Managerdaten muessen als JSON uebergeben werden.'
);
}
$daten = json_decode($wert, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($daten)) {
throw new InvalidArgumentException(
'Managerdaten muessen ein JSON-Objekt sein.'
);
}
$this->ManagerdatenEmpfangen($daten);
return;
}
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
}
public function ReceiveData($jsonString): void
{
$paket = json_decode((string) $jsonString, true);
if (!is_array($paket) || !isset($paket['Buffer'])) {
return;
}
$ereignis = json_decode((string) $paket['Buffer'], true);
if (!is_array($ereignis) || !isset($ereignis['type'])) {
return;
}
if ($ereignis['type'] === 'GatewayStatus') {
$verbunden = (bool) ($ereignis['connected'] ?? false);
$this->WriteAttributeBoolean('GatewayVerbunden', $verbunden);
$this->setzeSichtbareVariable('GatewayVerbunden', $verbunden);
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
if ($verbunden) {
$this->statusAnfordern();
} else {
$this->verwerfeBeobachtungen();
$this->aktualisiere(true);
}
return;
}
$seriennummer = EaseeGatewayProtokoll::seriennummer(
(string) ($ereignis['serialNumber'] ?? '')
);
if (
$ereignis['type'] !== 'Observation'
|| $seriennummer !== $this->seriennummer()
) {
return;
}
$this->speichereBeobachtung(
(int) ($ereignis['id'] ?? 0),
$ereignis['value'] ?? null,
(int) ($ereignis['timestamp'] ?? time())
);
$this->aktualisiere(true);
}
/** @param array<string, mixed> $daten */
public function ManagerdatenEmpfangen(array $daten): void
{
Nachrichtenvertrag::pruefeManagerdaten($daten);
if ($daten['Kopf']['EmpfaengerID'] !== $this->InstanceID) {
throw new InvalidArgumentException(
'Managerdaten sind an eine andere Instanz adressiert.'
);
}
$managerID = $daten['Kopf']['AbsenderID'];
if (!in_array($managerID, $this->zugeordneteManagerIDs(), true)) {
throw new InvalidArgumentException(
'Der Manager hat diese Ladestation nicht aktiv zugeordnet.'
);
}
$this->WriteAttributeString('Betriebsart', $daten['Betriebsart']);
$this->aktualisiere(false);
$sollleistung = $daten['Sollleistung_W'];
if ($sollleistung === null) {
if (
(bool) $this->leseZustand('SollwertGueltig')
&& !in_array(
(int) $this->leseZustand('Sollleistung'),
$this->leseLeistungsangebot(),
true
)
) {
$this->setzeZustand('SollwertGueltig', false);
$this->aktualisiere(false);
}
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
return;
}
if (!in_array($sollleistung, $this->leseLeistungsangebot(), true)) {
throw new InvalidArgumentException(
'Sollleistung_W liegt nicht im aktuell gemeldeten Leistungsangebot.'
);
}
$this->WriteAttributeInteger('LetzteManagerID', $managerID);
$this->WriteAttributeInteger('LetzteVorgabeZeit', time());
$this->setzeZustand('Sollleistung', $sollleistung);
$this->setzeZustand('SollwertGueltig', true);
$this->SetTimerInterval(
'VorgabeTimeout',
$this->ReadPropertyInteger('VorgabeTimeout') * 1000
);
$this->aktualisiere(false);
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
}
private function statusAnfordern(): void
{
try {
$antwort = $this->gatewayAnfrage([
'action' => 'Subscribe',
'serialNumber' => $this->seriennummer(),
]);
if (!($antwort['success'] ?? false)) {
throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee Gateway nicht erreichbar.')
);
}
$this->WriteAttributeBoolean(
'GatewayVerbunden',
(bool) ($antwort['connected'] ?? false)
);
$this->setzeSichtbareVariable(
'GatewayVerbunden',
$this->ReadAttributeBoolean('GatewayVerbunden')
);
$this->verwerfeBeobachtungen();
if (is_array($antwort['state'] ?? null)) {
$zustand = $antwort['state'];
$zeitpunkt = (int) ($zustand['updated'] ?? time());
foreach (EaseeGatewayProtokoll::BEOBACHTUNGEN as $id) {
if (array_key_exists((string) $id, $zustand)) {
$this->speichereBeobachtung(
$id,
$zustand[(string) $id],
$zeitpunkt
);
}
}
}
$this->aktualisiere(true);
} catch (Throwable $fehler) {
$this->WriteAttributeBoolean('GatewayVerbunden', false);
$this->aktualisiere(true);
$this->protokolliere('Gateway', $fehler->getMessage());
}
}
/** @param array<string, mixed> $anfrage
* @return array<string, mixed>
*/
private function gatewayAnfrage(array $anfrage): array
{
$antwort = $this->SendDataToParent(json_encode([
'DataID' => self::GATEWAY_REQUEST_DATA_ID,
'Buffer' => json_encode($anfrage, JSON_THROW_ON_ERROR),
], JSON_THROW_ON_ERROR));
if (!is_string($antwort) || $antwort === '') {
throw new RuntimeException('Easee Gateway liefert keine Antwort.');
}
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($daten)) {
throw new RuntimeException('Easee Gateway liefert ungueltige Daten.');
}
return $daten;
}
private function speichereBeobachtung(int $id, $wert, int $zeitpunkt): void
{
if (!in_array($id, EaseeGatewayProtokoll::BEOBACHTUNGEN, true)) {
return;
}
$beobachtungen = $this->leseBeobachtungen();
if ($id === 109 && (int) $wert === 1) {
foreach ([110, 120, 182, 183, 184, 185] as $sessionID) {
$beobachtungen[(string) $sessionID] = 0;
}
}
$beobachtungen[(string) $id] = $wert;
$this->WriteAttributeString(
'EaseeBeobachtungen',
json_encode($beobachtungen, JSON_THROW_ON_ERROR)
);
$this->WriteAttributeInteger('LetzteBeobachtung', $zeitpunkt);
}
private function aktualisiere(bool $meldungPlanen): void
{
try {
$status = EaseeLadestatus::ausBeobachtungen(
$this->leseBeobachtungen(),
$this->ReadPropertyInteger('MaximalerLadestrom')
);
foreach ([
'FahrzeugVerbunden',
'FahrzeugGeladen',
'Fahrzeugstatus',
'Phasenzahl',
'Istleistung_W',
'Ladestrom_A',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'GrundKeinLadestrom',
] as $feld) {
$this->setzeZustand($feld, $status[$feld]);
}
$this->WriteAttributeInteger(
'ApiMaximalstrom',
$status['ApiMaximalstrom_A']
);
$this->setzeSichtbareVariable(
'ApiMaximalstrom',
$status['ApiMaximalstrom_A']
);
if (!$status['FahrzeugVerbunden']) {
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', false);
}
$gatewayVerbunden = $this->ReadAttributeBoolean('GatewayVerbunden');
$phaseGueltig = in_array($status['Phasenzahl'], [1, 3], true);
$regelPhasenzahl = $phaseGueltig ? $status['Phasenzahl'] : 1;
$phaseErforderlich = $status['Fahrzeugstatus'] === 3;
$steuerbar = $gatewayVerbunden
&& $status['StatusGueltig']
&& $status['EaseeAktiviert']
&& !$status['Stoerung']
&& (!$phaseErforderlich || $phaseGueltig);
$solarladen = $this->ReadAttributeBoolean('ZustandSolarladen');
$peakbetrieb = $this->ReadAttributeString('Betriebsart')
=== Nachrichtenvertrag::BETRIEBSART_PEAK;
$angebot = LadestationRegler::leistungsangebot(
(bool) $this->GetValue('Aktiv'),
$this->ReadAttributeBoolean('ZustandLadefreigabe'),
$steuerbar && $status['FahrzeugVerbunden'],
$status['FahrzeugGeladen'],
$regelPhasenzahl,
$status['ApiMaximalstrom_A'],
$solarladen,
$peakbetrieb
);
$verfuegbar = $steuerbar
&& $status['FahrzeugVerbunden']
&& !$status['FahrzeugGeladen']
&& $angebot !== [0];
$angebotJson = json_encode($angebot, JSON_THROW_ON_ERROR);
$this->WriteAttributeString('Leistungsangebot', $angebotJson);
$this->setzeSichtbareVariable('LeistungsangebotDiagnose', $angebotJson);
$this->setzeZustand('Verfuegbar', $verfuegbar);
$this->setzeZustand(
'AenderungMoeglich',
$verfuegbar && count($angebot) > 1
);
$stoertext = '';
if (!$gatewayVerbunden) {
$stoertext = 'Easee Gateway ist nicht verbunden.';
$this->SetStatus(self::STATUS_GATEWAY_GETRENNT);
} elseif (!$status['StatusGueltig']) {
$stoertext = 'Warte auf den aktuellen Easee-Ladestatus.';
$this->SetStatus(self::STATUS_WARTET_AUF_DATEN);
} elseif ($status['Stoerung']) {
$stoertext = $status['Stoertext'];
$this->SetStatus(self::STATUS_LADEFEHLER);
} elseif ($phaseErforderlich && !$phaseGueltig) {
$stoertext = 'Easee meldet beim Laden keine unterstuetzte aktive Ausgangsphase.';
$this->SetStatus(self::STATUS_WARTET_AUF_DATEN);
} else {
$this->SetStatus(self::STATUS_AKTIV);
}
$this->setzeStoerung($stoertext);
if ($gatewayVerbunden && $status['StatusGueltig']) {
$this->setzeLadestrom(
$this->bestimmeWirksameSollleistung($angebot),
$regelPhasenzahl,
$status['Fahrzeugstatus'] === 2
);
} else {
$this->WriteAttributeInteger('LetzterGesetzterStrom', -1);
}
} catch (Throwable $fehler) {
$this->setzeZustand('Verfuegbar', false);
$this->setzeZustand('AenderungMoeglich', false);
$this->setzeStoerung($fehler->getMessage());
$this->SetStatus(self::STATUS_GATEWAY_GETRENNT);
$this->protokolliere('Easee', $fehler->getMessage());
}
if ($meldungPlanen) {
$this->SetTimerInterval('RueckmeldungVerzoegert', 100);
}
}
/** @param list<int> $angebot */
private function bestimmeWirksameSollleistung(array $angebot): int
{
if ((bool) $this->leseZustand('SollwertGueltig')) {
$sollleistung = (int) $this->leseZustand('Sollleistung');
if (in_array($sollleistung, $angebot, true)) {
return $sollleistung;
}
$this->setzeZustand('SollwertGueltig', false);
}
if (
!$this->ReadAttributeBoolean('ZustandSolarladen')
&& $angebot !== [0]
) {
return $angebot[count($angebot) - 1];
}
return 0;
}
private function setzeLadestrom(
int $leistung,
int $phasenzahl,
bool $wartetAufStart
): void {
$strom = $leistung === 0
? 0
: LadestationRegler::stromFuerLeistung($leistung, $phasenzahl);
if ($strom !== $this->ReadAttributeInteger('LetzterGesetzterStrom')) {
$antwort = $this->gatewayAnfrage([
'action' => 'SetDynamicChargerCurrent',
'serialNumber' => $this->seriennummer(),
'amps' => $strom,
]);
if (!($antwort['success'] ?? false)) {
throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee-Stromvorgabe fehlgeschlagen.')
);
}
$befehl = json_encode([
'Aktion' => 'SetDynamicChargerCurrent',
'Seriennummer' => $this->seriennummer(),
'Ampere' => $strom,
], JSON_THROW_ON_ERROR);
$this->WriteAttributeString('LetzterGeraetebefehl', $befehl);
$this->setzeSichtbareVariable('LetzterGeraetebefehl', $befehl);
$this->WriteAttributeInteger('LetzterGesetzterStrom', $strom);
}
if (
$strom > 0
&& $wartetAufStart
&& !$this->ReadAttributeBoolean('LadestartFuerVerbindungGesendet')
) {
$this->sendeLadestart();
}
}
private function sendeLadestart(): void
{
$antwort = $this->gatewayAnfrage([
'action' => 'StartCharging',
'serialNumber' => $this->seriennummer(),
]);
if (!($antwort['success'] ?? false)) {
throw new RuntimeException(
(string) ($antwort['error'] ?? 'Easee-Ladestart fehlgeschlagen.')
);
}
$befehl = json_encode([
'Aktion' => 'StartCharging',
'Seriennummer' => $this->seriennummer(),
], JSON_THROW_ON_ERROR);
$this->WriteAttributeBoolean('LadestartFuerVerbindungGesendet', true);
$this->WriteAttributeString('LetzterStartbefehl', $befehl);
$this->setzeSichtbareVariable('LetzterStartbefehl', $befehl);
}
private function seriennummer(): string
{
return EaseeGatewayProtokoll::seriennummer(
$this->ReadPropertyString('Ladestationskennung')
);
}
private function verwerfeBeobachtungen(): void
{
$this->WriteAttributeString('EaseeBeobachtungen', '{}');
$this->WriteAttributeInteger('LetzteBeobachtung', 0);
}
/** @return array<string, mixed> */
private function leseBeobachtungen(): array
{
$daten = json_decode($this->ReadAttributeString('EaseeBeobachtungen'), true);
return is_array($daten) ? $daten : [];
}
private function sendeVerbraucherdaten(): void
{
foreach ($this->zugeordneteManagerIDs() as $managerID) {
$daten = $this->baueVerbraucherdaten($managerID);
Nachrichtenvertrag::pruefeVerbraucherdaten($daten);
try {
IPS_RequestAction(
$managerID,
'VerbraucherdatenEmpfangen',
json_encode($daten, JSON_THROW_ON_ERROR)
);
} catch (Throwable $fehler) {
$this->protokolliere('Managerkommunikation', $fehler->getMessage());
}
}
}
/** @return array<string, mixed> */
private function baueVerbraucherdaten(int $managerID): array
{
$stoerung = (bool) $this->leseZustand('Stoerung');
return [
'Kopf' => [
'Version' => Nachrichtenvertrag::VERSION,
'AbsenderID' => $this->InstanceID,
'EmpfaengerID' => $managerID,
'Zeitpunkt' => time(),
],
'Betriebsart' => $this->ReadAttributeString('Betriebsart'),
'PrioritaetPV' => $this->ReadPropertyInteger('PrioritaetPV'),
'PrioritaetPeak' => $this->ReadPropertyInteger('PrioritaetPeak'),
'Leistungswerte_W' => $this->leseLeistungsangebot(),
'AenderungMoeglich' => (bool) $this->leseZustand('AenderungMoeglich'),
'Verfuegbar' => (bool) $this->leseZustand('Verfuegbar'),
'Istleistung_W' => (float) $this->leseZustand('Istleistung'),
'Leistungsquelle' => Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN,
'Zustand' => [
[
'Kennung' => 'Sollleistung_W',
'Art' => 'Sollwert',
'Wert' => (bool) $this->leseZustand('SollwertGueltig')
? (int) $this->leseZustand('Sollleistung')
: null,
'Einheit' => 'W',
],
[
'Kennung' => 'FahrzeugVerbunden',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('FahrzeugVerbunden'),
'Einheit' => '',
],
[
'Kennung' => 'FahrzeugGeladen',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('FahrzeugGeladen'),
'Einheit' => '',
],
[
'Kennung' => 'Fahrzeugstatus',
'Art' => 'Status',
'Wert' => (int) $this->leseZustand('Fahrzeugstatus'),
'Einheit' => '',
],
[
'Kennung' => 'Phasenzahl',
'Art' => 'Status',
'Wert' => (int) $this->leseZustand('Phasenzahl'),
'Einheit' => '',
],
[
'Kennung' => 'Ladestrom_A',
'Art' => 'Istwert',
'Wert' => (float) $this->leseZustand('Ladestrom'),
'Einheit' => 'A',
],
[
'Kennung' => 'Ladefreigabe',
'Art' => 'Status',
'Wert' => $this->ReadAttributeBoolean('ZustandLadefreigabe'),
'Einheit' => '',
],
[
'Kennung' => 'Solarladen',
'Art' => 'Status',
'Wert' => $this->ReadAttributeBoolean('ZustandSolarladen'),
'Einheit' => '',
],
[
'Kennung' => 'EaseeAktiviert',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('EaseeAktiviert'),
'Einheit' => '',
],
[
'Kennung' => 'AutorisierungErforderlich',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('AutorisierungErforderlich'),
'Einheit' => '',
],
[
'Kennung' => 'RemoteStartErforderlich',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('RemoteStartErforderlich'),
'Einheit' => '',
],
[
'Kennung' => 'SmartCharging',
'Art' => 'Status',
'Wert' => (bool) $this->leseZustand('SmartCharging'),
'Einheit' => '',
],
[
'Kennung' => 'GrundKeinLadestrom',
'Art' => 'Status',
'Wert' => (int) $this->leseZustand('GrundKeinLadestrom'),
'Einheit' => '',
],
[
'Kennung' => 'Ladefehler',
'Art' => 'Stoerung',
'Wert' => $stoerung,
'Einheit' => '',
'Text' => (string) $this->leseZustand('Stoertext'),
],
],
];
}
/** @return list<int> */
private function zugeordneteManagerIDs(): array
{
$ergebnis = [];
foreach (IPS_GetInstanceListByModuleID(self::MANAGER_MODULE_ID) as $managerID) {
try {
$automatisch = (bool) IPS_GetProperty(
$managerID,
'AutomatischeSuche'
);
$property = $automatisch
? 'AutomatischeVerbraucherZuordnung'
: 'VerbraucherZuordnung';
$zuordnung = $this->leseManagerZuordnung($managerID, $property);
if ($automatisch && $zuordnung === []) {
$zuordnung = $this->leseManagerZuordnung(
$managerID,
'VerbraucherZuordnung'
);
}
} catch (Throwable $fehler) {
continue;
}
foreach ($zuordnung as $eintrag) {
if (!is_array($eintrag) || ($eintrag['Aktiv'] ?? false) !== true) {
continue;
}
$instanzID = $eintrag['InstanzID'] ?? $eintrag['Verbraucher'] ?? null;
if ($instanzID === $this->InstanceID) {
$ergebnis[] = (int) $managerID;
break;
}
}
}
return array_values(array_unique($ergebnis));
}
/** @return array<mixed> */
private function leseManagerZuordnung(int $managerID, string $property): array
{
$zuordnung = json_decode(
(string) IPS_GetProperty($managerID, $property),
true,
512,
JSON_THROW_ON_ERROR
);
return is_array($zuordnung) ? $zuordnung : [];
}
private function pruefeKonfiguration(): void
{
foreach (['PrioritaetPV', 'PrioritaetPeak'] as $property) {
if ($this->ReadPropertyInteger($property) < 0) {
throw new InvalidArgumentException(
$property . ' muss mindestens 0 sein.'
);
}
}
foreach (['Meldeintervall', 'VorgabeTimeout'] as $property) {
if ($this->ReadPropertyInteger($property) <= 0) {
throw new InvalidArgumentException(
$property . ' muss groesser als 0 sein.'
);
}
}
$maximalstrom = $this->ReadPropertyInteger('MaximalerLadestrom');
if ($maximalstrom < 6 || $maximalstrom > 32) {
throw new InvalidArgumentException(
'MaximalerLadestrom muss zwischen 6 und 32 A liegen.'
);
}
EaseeLadestatus::solarladenErzwungen(
$this->ReadPropertyInteger('Betriebsmodus')
);
if ($this->seriennummer() === '') {
throw new InvalidArgumentException('Easee Seriennummer fehlt.');
}
}
private function initialisiereLokaleEinstellungen(): void
{
$initialisiert = $this->ReadAttributeBoolean('InitialwerteGesetzt');
$ladefreigabe = $this->ReadPropertyBoolean('Ladefreigabe');
$solarladen = EaseeLadestatus::solarladenErzwungen(
$this->ReadPropertyInteger('Betriebsmodus')
) ? true : $this->ReadPropertyBoolean('Solarladen');
if (
!$initialisiert
|| $ladefreigabe
!== $this->ReadAttributeBoolean('KonfigurationLadefreigabe')
) {
$this->WriteAttributeBoolean('ZustandLadefreigabe', $ladefreigabe);
}
if (
!$initialisiert
|| $solarladen
!== $this->ReadAttributeBoolean('KonfigurationSolarladen')
) {
$this->WriteAttributeBoolean('ZustandSolarladen', $solarladen);
}
$this->WriteAttributeBoolean('KonfigurationLadefreigabe', $ladefreigabe);
$this->WriteAttributeBoolean('KonfigurationSolarladen', $solarladen);
$this->WriteAttributeBoolean('InitialwerteGesetzt', true);
}
private function aktualisiereVariablen(): void
{
if ($this->ReadPropertyBoolean('EinstellungenInVisu')) {
$this->RegisterVariableBoolean(
'Ladefreigabe',
'Ladefreigabe',
'~Switch',
150
);
$this->RegisterVariableBoolean(
'Solarladen',
'Solarladen',
'~Switch',
160
);
$this->EnableAction('Ladefreigabe');
$this->EnableAction('Solarladen');
$this->SetValue(
'Ladefreigabe',
$this->ReadAttributeBoolean('ZustandLadefreigabe')
);
$this->SetValue(
'Solarladen',
$this->ReadAttributeBoolean('ZustandSolarladen')
);
} else {
$this->entferneVariable('Ladefreigabe');
$this->entferneVariable('Solarladen');
}
if ($this->ReadPropertyBoolean('DiagnosevariablenAnzeigen')) {
$this->registriereVerbraucherDiagnose();
$this->RegisterVariableBoolean(
'GatewayVerbunden',
'Easee Gateway verbunden',
'~Switch',
200
);
$this->RegisterVariableInteger(
'ApiMaximalstrom',
'API-Maximalstrom',
'',
210
);
$this->RegisterVariableBoolean(
'EaseeAktiviert',
'Easee aktiviert',
'~Switch',
220
);
$this->RegisterVariableBoolean(
'AutorisierungErforderlich',
'Autorisierung erforderlich',
'~Switch',
230
);
$this->RegisterVariableBoolean(
'RemoteStartErforderlich',
'Remote-Start erforderlich',
'~Switch',
240
);
$this->RegisterVariableBoolean(
'SmartCharging',
'Easee Smart Charging',
'~Switch',
250
);
$this->RegisterVariableInteger(
'GrundKeinLadestrom',
'Grund kein Ladestrom',
'',
260
);
$this->RegisterVariableString(
'LetzterGeraetebefehl',
'Letzter Geraetebefehl',
'',
270
);
$this->RegisterVariableString(
'LetzterStartbefehl',
'Letzter Startbefehl',
'',
280
);
$this->RegisterVariableString(
'LeistungsangebotDiagnose',
'Leistungsangebot',
'',
290
);
$this->SetValue(
'LeistungsangebotDiagnose',
$this->ReadAttributeString('Leistungsangebot')
);
$this->SetValue(
'LetzterGeraetebefehl',
$this->ReadAttributeString('LetzterGeraetebefehl')
);
$this->SetValue(
'LetzterStartbefehl',
$this->ReadAttributeString('LetzterStartbefehl')
);
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
if (!in_array($ident, [
'GatewayVerbunden',
'ApiMaximalstrom',
'LetzterGeraetebefehl',
'LetzterStartbefehl',
'LeistungsangebotDiagnose',
], true)) {
$this->setzeSichtbareVariable($ident, $this->leseZustand($ident));
}
}
$this->setzeSichtbareVariable(
'GatewayVerbunden',
$this->ReadAttributeBoolean('GatewayVerbunden')
);
$this->setzeSichtbareVariable(
'ApiMaximalstrom',
$this->ReadAttributeInteger('ApiMaximalstrom')
);
} else {
foreach (self::DIAGNOSE_VARIABLEN as $ident) {
$this->entferneVariable($ident);
}
}
}
private function entferneVariable(string $ident): void
{
$id = @$this->GetIDForIdent($ident);
if (is_int($id) && $id > 0 && IPS_VariableExists($id)) {
$this->UnregisterVariable($ident);
}
}
/** @param mixed $wert */
private function setzeZustand(string $ident, $wert): void
{
switch ($ident) {
case 'FahrzeugVerbunden':
case 'FahrzeugGeladen':
case 'EaseeAktiviert':
case 'AutorisierungErforderlich':
case 'RemoteStartErforderlich':
case 'SmartCharging':
case 'SollwertGueltig':
case 'Verfuegbar':
case 'AenderungMoeglich':
case 'Stoerung':
$this->WriteAttributeBoolean('Zustand' . $ident, (bool) $wert);
break;
case 'Fahrzeugstatus':
case 'Phasenzahl':
case 'GrundKeinLadestrom':
case 'Sollleistung':
$this->WriteAttributeInteger('Zustand' . $ident, (int) $wert);
break;
case 'Ladestrom_A':
$ident = 'Ladestrom';
$this->WriteAttributeFloat('ZustandLadestrom', (float) $wert);
break;
case 'Istleistung_W':
$ident = 'Istleistung';
$this->WriteAttributeFloat('ZustandIstleistung', (float) $wert);
break;
case 'Stoertext':
$this->WriteAttributeString('ZustandStoertext', (string) $wert);
break;
default:
throw new LogicException('Unbekannter Zustand: ' . $ident);
}
$this->setzeSichtbareVariable($ident, $wert);
if ($ident === 'Istleistung') {
$this->setzeSichtbareVariable(
'Leistungsquelle',
Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN
);
}
}
/** @return mixed */
private function leseZustand(string $ident)
{
if ($ident === 'Ladefreigabe' || $ident === 'Solarladen') {
return $this->ReadAttributeBoolean('Zustand' . $ident);
}
if (in_array($ident, [
'FahrzeugVerbunden',
'FahrzeugGeladen',
'EaseeAktiviert',
'AutorisierungErforderlich',
'RemoteStartErforderlich',
'SmartCharging',
'SollwertGueltig',
'Verfuegbar',
'AenderungMoeglich',
'Stoerung',
], true)) {
return $this->ReadAttributeBoolean('Zustand' . $ident);
}
if (in_array($ident, [
'Fahrzeugstatus',
'Phasenzahl',
'GrundKeinLadestrom',
'Sollleistung',
], true)) {
return $this->ReadAttributeInteger('Zustand' . $ident);
}
if ($ident === 'Ladestrom' || $ident === 'Istleistung') {
return $this->ReadAttributeFloat('Zustand' . $ident);
}
if ($ident === 'Stoertext') {
return $this->ReadAttributeString('ZustandStoertext');
}
if ($ident === 'Leistungsquelle') {
return Nachrichtenvertrag::LEISTUNGSQUELLE_GEMESSEN;
}
throw new LogicException('Unbekannter Zustand: ' . $ident);
}
/** @param mixed $wert */
private function setzeSichtbareVariable(string $ident, $wert): void
{
$id = @$this->GetIDForIdent($ident);
if (is_int($id) && $id > 0 && IPS_VariableExists($id)) {
$this->SetValue($ident, $wert);
}
}
/** @return list<int> */
private function leseLeistungsangebot(): array
{
$angebot = json_decode(
$this->ReadAttributeString('Leistungsangebot'),
true,
512,
JSON_THROW_ON_ERROR
);
return is_array($angebot) ? array_map('intval', $angebot) : [0];
}
private function setzeStoerung(string $text): void
{
$this->setzeZustand('Stoerung', $text !== '');
$this->setzeZustand('Stoertext', $text);
}
private function deaktiviereTimer(): void
{
foreach (['Meldezyklus', 'RueckmeldungVerzoegert', 'VorgabeTimeout'] as $timer) {
$this->SetTimerInterval($timer, 0);
}
}
private function protokolliere(string $bereich, string $nachricht): void
{
if ($this->ReadPropertyBoolean('LoggingEin')) {
$this->SendDebug($bereich, $nachricht, 0);
}
}
}