This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/libs/VGTRegler.php';
|
||||
|
||||
use Belevo\EnelixUtils\VGTRegler;
|
||||
|
||||
class VGTSchnittstelle extends IPSModule
|
||||
{
|
||||
private const STATUS_AKTIV = 102;
|
||||
private const STATUS_KONFIGURATION_UNGUELTIG = 201;
|
||||
private const STATUS_AUSGABE_FEHLER = 202;
|
||||
|
||||
public function Create(): void
|
||||
{
|
||||
parent::Create();
|
||||
|
||||
$this->RegisterPropertyInteger(
|
||||
'Geraeteart',
|
||||
VGTRegler::GERAETEART_PV
|
||||
);
|
||||
$this->RegisterPropertyString('TopicSuffix', '');
|
||||
$this->RegisterPropertyInteger('ReqActionID', 0);
|
||||
$this->RegisterPropertyInteger('PowerProductionID', 0);
|
||||
$this->RegisterPropertyInteger('SoCID', 0);
|
||||
$this->RegisterPropertyFloat('TargetSoC', 50.0);
|
||||
$this->RegisterPropertyInteger('ChargePower', 2500);
|
||||
$this->RegisterPropertyInteger('DischargePower', 2500);
|
||||
$this->RegisterPropertyInteger('MaxPowerSetpoint', 10000);
|
||||
$this->RegisterPropertyBoolean('LoggingEin', false);
|
||||
|
||||
$this->RegisterVariableBoolean('IsReady', 'Is Ready', '', 10);
|
||||
$this->RegisterVariableBoolean('IsRunning', 'Is Running', '', 20);
|
||||
$this->RegisterVariableFloat('MinSoC', 'Min SoC', '', 30);
|
||||
$this->EnableAction('MinSoC');
|
||||
$this->RegisterVariableFloat('MaxSoC', 'Max SoC', '', 40);
|
||||
$this->EnableAction('MaxSoC');
|
||||
$this->RegisterVariableInteger(
|
||||
'PowerSetpoint',
|
||||
'Power Setpoint',
|
||||
'',
|
||||
50
|
||||
);
|
||||
$this->EnableAction('PowerSetpoint');
|
||||
$this->RegisterVariableString('Strategy', 'Strategy', '', 60);
|
||||
$this->EnableAction('Strategy');
|
||||
$this->RegisterVariableString(
|
||||
'LastReadResponse',
|
||||
'Letzte Lese-Antwort',
|
||||
'',
|
||||
70
|
||||
);
|
||||
$this->RegisterVariableString(
|
||||
'LastWriteResponse',
|
||||
'Letzte Steuer-Antwort',
|
||||
'',
|
||||
80
|
||||
);
|
||||
$this->RegisterVariableBoolean('Stoerung', 'Stoerung', '~Switch', 90);
|
||||
$this->RegisterVariableString('Stoertext', 'Stoertext', '', 100);
|
||||
|
||||
$this->RegisterAttributeString('PublishQueue', '[]');
|
||||
$this->RegisterAttributeFloat('LetzterZielwert', 0.0);
|
||||
$this->RegisterAttributeBoolean('SoCGrenzenInitialisiert', false);
|
||||
|
||||
$this->ConnectParent('{F7A0DD2E-7684-95C0-64C2-D2A9DC47577B}');
|
||||
$this->RegisterTimer(
|
||||
'PublishDelay',
|
||||
0,
|
||||
'ENLVGT_DoDelayedPublish($_IPS["TARGET"]);'
|
||||
);
|
||||
$this->RegisterTimer(
|
||||
'BatteryControlTimer',
|
||||
0,
|
||||
'ENLVGT_RunControl($_IPS["TARGET"]);'
|
||||
);
|
||||
}
|
||||
|
||||
public function ApplyChanges(): void
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
try {
|
||||
if (!$this->ReadAttributeBoolean('SoCGrenzenInitialisiert')) {
|
||||
$this->SetValue('MinSoC', 0.0);
|
||||
$this->SetValue('MaxSoC', 100.0);
|
||||
$this->WriteAttributeBoolean('SoCGrenzenInitialisiert', true);
|
||||
}
|
||||
$this->pruefeKonfiguration();
|
||||
$suffix = $this->ReadPropertyString('TopicSuffix');
|
||||
$filter = '.*"Topic":"(feedback-request|remote-control-request)\\/'
|
||||
. preg_quote($suffix, '/')
|
||||
. '".*';
|
||||
$this->SetReceiveDataFilter($filter);
|
||||
$this->SetTimerInterval('BatteryControlTimer', 3000);
|
||||
$this->SetValue('IsReady', true);
|
||||
$this->setzeFehler('');
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
$this->RunControl();
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetTimerInterval('BatteryControlTimer', 0);
|
||||
$this->SetValue('IsReady', false);
|
||||
$this->SetValue('IsRunning', false);
|
||||
$this->setzeFehler($fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_KONFIGURATION_UNGUELTIG);
|
||||
}
|
||||
}
|
||||
|
||||
public function RequestAction($ident, $wert): void
|
||||
{
|
||||
switch ($ident) {
|
||||
case 'PowerSetpoint':
|
||||
$begrenzt = VGTRegler::begrenzeSetpoint(
|
||||
$this->ReadPropertyInteger('Geraeteart'),
|
||||
(float) $wert,
|
||||
(float) $this->ReadPropertyInteger('MaxPowerSetpoint')
|
||||
);
|
||||
$this->SetValue('PowerSetpoint', (int) round($begrenzt));
|
||||
$this->RunControl();
|
||||
return;
|
||||
|
||||
case 'Strategy':
|
||||
$this->SetValue('Strategy', trim((string) $wert));
|
||||
$this->RunControl();
|
||||
return;
|
||||
|
||||
case 'MinSoC':
|
||||
$this->SetValue(
|
||||
'MinSoC',
|
||||
min(100.0, max(0.0, (float) $wert))
|
||||
);
|
||||
$this->RunControl();
|
||||
return;
|
||||
|
||||
case 'MaxSoC':
|
||||
$this->SetValue(
|
||||
'MaxSoC',
|
||||
min(100.0, max(0.0, (float) $wert))
|
||||
);
|
||||
$this->RunControl();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Aktion: ' . $ident);
|
||||
}
|
||||
|
||||
public function RunControl(): void
|
||||
{
|
||||
try {
|
||||
$this->pruefeKonfiguration();
|
||||
$geraeteart = $this->ReadPropertyInteger('Geraeteart');
|
||||
$strategie = (string) $this->GetValue('Strategy');
|
||||
if ($geraeteart === VGTRegler::GERAETEART_PV
|
||||
&& !in_array($strategie, ['activate', 'stop'], true)
|
||||
) {
|
||||
$this->SetValue('IsReady', true);
|
||||
$this->SetValue('IsRunning', false);
|
||||
$this->setzeFehler('');
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
return;
|
||||
}
|
||||
|
||||
$ladezustand = $geraeteart === VGTRegler::GERAETEART_BATTERIE
|
||||
? $this->leseNumerisch($this->ReadPropertyInteger('SoCID'))
|
||||
: 0.0;
|
||||
$zielwert = VGTRegler::zielwert(
|
||||
$geraeteart,
|
||||
$strategie,
|
||||
(float) $this->GetValue('PowerSetpoint'),
|
||||
(float) $this->ReadPropertyInteger('MaxPowerSetpoint'),
|
||||
$ladezustand,
|
||||
$this->ReadPropertyFloat('TargetSoC'),
|
||||
(float) $this->ReadPropertyInteger('ChargePower'),
|
||||
(float) $this->ReadPropertyInteger('DischargePower'),
|
||||
(float) $this->GetValue('MinSoC'),
|
||||
(float) $this->GetValue('MaxSoC')
|
||||
);
|
||||
|
||||
RequestAction(
|
||||
$this->ReadPropertyInteger('ReqActionID'),
|
||||
$zielwert
|
||||
);
|
||||
$this->WriteAttributeFloat('LetzterZielwert', $zielwert);
|
||||
$this->SetValue('IsReady', true);
|
||||
$this->SetValue('IsRunning', abs($zielwert) >= 0.5);
|
||||
$this->setzeFehler('');
|
||||
$this->SetStatus(self::STATUS_AKTIV);
|
||||
$this->protokolliere('Zielwert', $zielwert);
|
||||
} catch (Throwable $fehler) {
|
||||
$this->SetValue('IsRunning', false);
|
||||
$this->setzeFehler($fehler->getMessage());
|
||||
$this->SetStatus(self::STATUS_AUSGABE_FEHLER);
|
||||
$this->protokolliere('Steuerfehler', $fehler->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function BuildReadResponse(): string
|
||||
{
|
||||
$data = [
|
||||
'power_production' => $this->leseOptionalNumerisch(
|
||||
$this->ReadPropertyInteger('PowerProductionID')
|
||||
),
|
||||
'is_ready' => (bool) $this->GetValue('IsReady'),
|
||||
'is_running' => (bool) $this->GetValue('IsRunning'),
|
||||
];
|
||||
|
||||
if ($this->ReadPropertyInteger('Geraeteart')
|
||||
=== VGTRegler::GERAETEART_BATTERIE
|
||||
) {
|
||||
$data['state_of_charge'] = $this->leseOptionalNumerisch(
|
||||
$this->ReadPropertyInteger('SoCID')
|
||||
);
|
||||
$data['min_soc'] = (float) $this->GetValue('MinSoC');
|
||||
$data['max_soc'] = (float) $this->GetValue('MaxSoC');
|
||||
}
|
||||
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
|
||||
$this->SetValue('LastReadResponse', $json);
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function HandleRemoteControlJSON(string $payload): ?string
|
||||
{
|
||||
if ($payload === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($payload, true);
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists('power_setpoint', $data)) {
|
||||
$wert = VGTRegler::begrenzeSetpoint(
|
||||
$this->ReadPropertyInteger('Geraeteart'),
|
||||
(float) $data['power_setpoint'],
|
||||
(float) $this->ReadPropertyInteger('MaxPowerSetpoint')
|
||||
);
|
||||
$this->SetValue('PowerSetpoint', (int) round($wert));
|
||||
}
|
||||
if (array_key_exists('strategy', $data)) {
|
||||
$this->SetValue('Strategy', (string) $data['strategy']);
|
||||
}
|
||||
|
||||
$this->RunControl();
|
||||
|
||||
$antwort = [
|
||||
'power_setpoint' => (int) $this->GetValue('PowerSetpoint'),
|
||||
'strategy' => (string) $this->GetValue('Strategy'),
|
||||
];
|
||||
$json = json_encode(
|
||||
$antwort,
|
||||
JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
|
||||
);
|
||||
$this->SetValue('LastWriteResponse', $json);
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function ReceiveData($jsonString): void
|
||||
{
|
||||
$data = json_decode((string) $jsonString, true);
|
||||
if (!is_array($data)
|
||||
|| (int) ($data['PacketType'] ?? 0) !== 3
|
||||
|| !isset($data['Topic'])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$topic = (string) $data['Topic'];
|
||||
$payload = (string) ($data['Payload'] ?? '');
|
||||
$suffix = $this->ReadPropertyString('TopicSuffix');
|
||||
$this->protokolliere('ReceiveData', $jsonString);
|
||||
|
||||
if ($topic === 'feedback-request/' . $suffix) {
|
||||
$this->QueuePublish(
|
||||
'feedback-response/' . $suffix,
|
||||
$this->BuildReadResponse()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($topic === 'remote-control-request/' . $suffix) {
|
||||
$antwort = $this->HandleRemoteControlJSON($payload);
|
||||
if ($antwort !== null) {
|
||||
$this->QueuePublish(
|
||||
'remote-control-response/' . $suffix,
|
||||
$antwort
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function QueuePublish(string $topic, string $payload): void
|
||||
{
|
||||
$queue = json_decode(
|
||||
$this->ReadAttributeString('PublishQueue'),
|
||||
true
|
||||
);
|
||||
if (!is_array($queue)) {
|
||||
$queue = [];
|
||||
}
|
||||
$queue[] = ['Topic' => $topic, 'Payload' => $payload];
|
||||
if (count($queue) > 100) {
|
||||
array_shift($queue);
|
||||
}
|
||||
$this->WriteAttributeString(
|
||||
'PublishQueue',
|
||||
json_encode($queue, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->SetTimerInterval('PublishDelay', 50);
|
||||
}
|
||||
|
||||
public function DoDelayedPublish(): void
|
||||
{
|
||||
$queue = json_decode(
|
||||
$this->ReadAttributeString('PublishQueue'),
|
||||
true
|
||||
);
|
||||
if (!is_array($queue) || $queue === []) {
|
||||
$this->SetTimerInterval('PublishDelay', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$eintrag = array_shift($queue);
|
||||
$this->WriteAttributeString(
|
||||
'PublishQueue',
|
||||
json_encode($queue, JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$this->SendDataToParent(json_encode([
|
||||
'DataID' => '{043EA491-0325-4ADD-8FC2-A30C8EEB4D3F}',
|
||||
'PacketType' => 3,
|
||||
'QualityOfService' => 0,
|
||||
'Retain' => false,
|
||||
'Topic' => (string) $eintrag['Topic'],
|
||||
'Payload' => (string) $eintrag['Payload'],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
$this->SetTimerInterval('PublishDelay', $queue === [] ? 0 : 50);
|
||||
}
|
||||
|
||||
private function pruefeKonfiguration(): void
|
||||
{
|
||||
VGTRegler::begrenzeSetpoint(
|
||||
$this->ReadPropertyInteger('Geraeteart'),
|
||||
0.0,
|
||||
(float) $this->ReadPropertyInteger('MaxPowerSetpoint')
|
||||
);
|
||||
if (trim($this->ReadPropertyString('TopicSuffix')) === '') {
|
||||
throw new InvalidArgumentException(
|
||||
'TopicSuffix ist nicht eingerichtet.'
|
||||
);
|
||||
}
|
||||
$this->pruefeAktionsvariable(
|
||||
$this->ReadPropertyInteger('ReqActionID'),
|
||||
'ReqActionID'
|
||||
);
|
||||
if ($this->ReadPropertyInteger('Geraeteart')
|
||||
=== VGTRegler::GERAETEART_BATTERIE
|
||||
) {
|
||||
$this->pruefeNumerischeVariable(
|
||||
$this->ReadPropertyInteger('SoCID'),
|
||||
'SoCID'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function pruefeAktionsvariable(int $id, string $name): void
|
||||
{
|
||||
$this->pruefeNumerischeVariable($id, $name);
|
||||
$variable = IPS_GetVariable($id);
|
||||
if ((int) ($variable['VariableAction'] ?? 0) <= 0
|
||||
&& (int) ($variable['VariableCustomAction'] ?? 0) <= 0
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
$name . ' benoetigt eine Variablenaktion.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function pruefeNumerischeVariable(int $id, string $name): void
|
||||
{
|
||||
if ($id <= 0 || !IPS_VariableExists($id)) {
|
||||
throw new InvalidArgumentException($name . ' ist nicht eingerichtet.');
|
||||
}
|
||||
$variable = IPS_GetVariable($id);
|
||||
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
$name . ' muss numerisch sein.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function leseNumerisch(int $id): float
|
||||
{
|
||||
$this->pruefeNumerischeVariable($id, 'Messvariable');
|
||||
return (float) GetValue($id);
|
||||
}
|
||||
|
||||
private function leseOptionalNumerisch(int $id): float
|
||||
{
|
||||
if ($id <= 0 || !IPS_VariableExists($id)) {
|
||||
return 0.0;
|
||||
}
|
||||
$variable = IPS_GetVariable($id);
|
||||
if (!in_array((int) $variable['VariableType'], [1, 2], true)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (float) GetValue($id);
|
||||
}
|
||||
|
||||
private function setzeFehler(string $text): void
|
||||
{
|
||||
$this->SetValue('Stoerung', $text !== '');
|
||||
$this->SetValue('Stoertext', $text);
|
||||
}
|
||||
|
||||
/** @param mixed $daten */
|
||||
private function protokolliere(string $titel, $daten): void
|
||||
{
|
||||
if (!$this->ReadPropertyBoolean('LoggingEin')) {
|
||||
return;
|
||||
}
|
||||
$text = is_string($daten)
|
||||
? $daten
|
||||
: json_encode($daten, JSON_THROW_ON_ERROR);
|
||||
$this->SendDebug($titel, $text, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user