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); } }