diff --git a/SofarWechselrichter/module.php b/SofarWechselrichter/module.php index 8c28087..1820e60 100644 --- a/SofarWechselrichter/module.php +++ b/SofarWechselrichter/module.php @@ -12,32 +12,26 @@ class SofarWechselrichter extends IPSModule $this->RegisterPropertyInteger('PollInterval', 60); $this->RegisterPropertyString('Registers', '[]'); // JSON-String - // Timer für zyklische Abfragen - $this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);'); + // Timer für zyklische Abfragen; ruft per RequestAction ident "Query" auf + $script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");'; + $this->RegisterTimer('QueryTimer', 0, $script); } public function ApplyChanges() { parent::ApplyChanges(); - // Timer-Intervall (ms) setzen $intervalSec = $this->ReadPropertyInteger('PollInterval'); $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; $this->SetTimerInterval('QueryTimer', $intervalMs); - // Variable für vorherigen Wert (Register 1160) anlegen, falls noch nicht vorhanden - if (!$this->VariableExists('PrevValue1160')) { - $this->RegisterVariableInteger('PrevValue1160', 'Vorheriger Wert (Reg 1160)', '', 10); - } - - // Alle Einträge aus „Registers“-Liste aus Property lesen + // Variablen für jeden definierten Register-Eintrag anlegen $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { $registers = []; } - // Für jeden Eintrag: Variable anlegen, falls nicht existiert - $position = 20; + $position = 10; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim($entry['Label']); @@ -58,45 +52,45 @@ class SofarWechselrichter extends IPSModule } /** - * Timer-Callback: wird in Intervallen aufgerufen + * Wird aufgerufen durch IPS_RequestAction über Timer */ - public function Query(): void + public function RequestAction($Ident, $Value) + { + switch ($Ident) { + case 'Query': + $this->Query(); + break; + + default: + throw new Exception('Ungültiger Ident: ' . $Ident); + } + } + + /** + * Führt eine Abfrage aller in der Tabelle definierten Register durch + */ + private function Query(): void { $ip = $this->ReadPropertyString('IPAddress'); $loggerNumber = $this->ReadPropertyInteger('LoggerNumber'); - // Ohne gültige Seriennummer abbrechen - if ($loggerNumber <= 0) { - $this->LogMessage('LoggerNumber ist ≤ 0, Abbruch', KL_WARNING); + if ($loggerNumber <= 0 || filter_var($ip, FILTER_VALIDATE_IP) === false) { + $this->LogMessage('LoggerNumber ≤ 0 oder IP ungültig, Abbruch', KL_WARNING); return; } - // (1) Register 1160 als INT16BE auslesen → „Vorheriger Wert“ - try { - $bytes1160 = $this->readRegister($ip, $loggerNumber, 1160, 'BE'); - $arr1160 = unpack('nvalue', $bytes1160); - $raw1160 = $arr1160['value']; - // Vorzeichenkorrektur - if ($raw1160 & 0x8000) { - $raw1160 -= 0x10000; - } - SetValue($this->GetIDForIdent('PrevValue1160'), $raw1160); - } catch (Exception $e) { - $this->LogMessage('Fehler Lesen Reg 1160: ' . $e->getMessage(), KL_WARNING); - } - - // (2) Alle Einträge aus der Registerliste abfragen $registers = json_decode($this->ReadPropertyString('Registers'), true); - if (!is_array($registers)) { + if (!is_array($registers) || count($registers) === 0) { + // Keine Register definiert return; } foreach ($registers as $entry) { - $regNo = (int) $entry['RegisterNumber']; - $label = trim((string)$entry['Label']); - $scale = (float) $entry['ScalingFactor']; - $endian = strtoupper(trim((string)$entry['Endian'])); - $ident = 'Reg' . $regNo; + $regNo = (int) $entry['RegisterNumber']; + $label = trim((string)$entry['Label']); + $scale = (float) $entry['ScalingFactor']; + $endian = strtoupper(trim((string)$entry['Endian'])); + $ident = 'Reg' . $regNo; if ($regNo < 0 || $label === '') { continue; @@ -105,9 +99,9 @@ class SofarWechselrichter extends IPSModule try { $bytes = $this->readRegister($ip, $loggerNumber, $regNo, $endian); if ($endian === 'LE') { - $arr = unpack('vvalue', $bytes); // uint16 little-endian + $arr = unpack('vvalue', $bytes); // UINT16 LE } else { - $arr = unpack('nvalue', $bytes); // uint16 big-endian + $arr = unpack('nvalue', $bytes); // UINT16 BE } $valueRaw = $arr['value']; $value = $valueRaw * $scale; @@ -119,14 +113,14 @@ class SofarWechselrichter extends IPSModule } /** - * Liest genau 2 Bytes eines Registers (Modbus-ähnlich über TCP, Port fix 8899). + * Liest ein einzelnes Register per Modbus-ähnlichem TCP (2 Bytes zurück) * - * @param string $ip IP-Adresse des Inverters - * @param int $serial_nr Seriennummer (Logger-Nummer) - * @param int $reg Register-Adresse - * @param string $endian 'BE' oder 'LE' - * @return string Binärkette mit genau 2 Bytes - * @throws Exception Bei Kommunikationsfehlern + * @param string $ip Inverter-IP + * @param int $serial_nr Logger-Seriennummer + * @param int $reg Register-Adresse + * @param string $endian 'BE' oder 'LE' + * @return string 2 Byte binär + * @throws Exception Bei Kommunikationsfehler */ private function readRegister( string $ip, @@ -137,8 +131,6 @@ class SofarWechselrichter extends IPSModule { // 1) Out_Frame ohne CRC aufbauen $oFrame = 'a5170010450000'; - - // Seriennummer (8-stelliges Hex), Byte-Little-Endian pro Byte $hexSN = str_pad(dechex($serial_nr), 8, '0', STR_PAD_LEFT); $hexSNbytes = [ substr($hexSN, 6, 2), @@ -147,14 +139,9 @@ class SofarWechselrichter extends IPSModule substr($hexSN, 0, 2), ]; $oFrame .= implode('', $hexSNbytes); - - // Data-Field (16 Hex-Zeichen konstant) $oFrame .= '020000000000000000000000000000'; - - // Business-Field: 01 03 + Start-Register (2 Bytes) + Anzahl Register (2 Bytes = 1) $startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT); - $numRegs = 1; - $numHex = str_pad(dechex($numRegs), 4, '0', STR_PAD_LEFT); + $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); $oFrame .= '0103' . $startHex . $numHex; // 2) CRC16-Modbus (letzte 6 Bytes) @@ -163,14 +150,13 @@ class SofarWechselrichter extends IPSModule if ($crcInputBin === false) { throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}"); } - $crcValue = $this->calculateCRC16Modbus($crcInputBin); - $crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT)); - // Byte-Swap (Low-Byte zuerst) - $crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2); + $crcValue = $this->calculateCRC16Modbus($crcInputBin); + $crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT)); + $crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2); $oFrameWithCRC = $oFrame . strtolower($crcSwapped); - // 3) Summen-Checksum (alle Bytes ab Index 1) + 0x15 - $l = strlen($oFrameWithCRC) / 2; + // 3) Summen-Checksum (Bytes ab Index 1) + 0x15 + $l = strlen($oFrameWithCRC) / 2; $bArr = []; for ($i = 0; $i < $l; $i++) { $byteHex = substr($oFrameWithCRC, 2 * $i, 2); @@ -189,10 +175,9 @@ class SofarWechselrichter extends IPSModule $frameBin .= chr($b); } - // 4) TCP-Verbindung öffnen & Paket senden (Port ist fest auf 8899) + // 4) TCP-Verbindung öffnen & Paket senden (Port fest 8899) $port = 8899; - $timeoutSec = 5; - $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeoutSec); + $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, 5); if (!$fp) { throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})"); } @@ -209,30 +194,25 @@ class SofarWechselrichter extends IPSModule $response .= $chunk; } fclose($fp); - if ($response === '') { throw new Exception("Keine Antwort vom Inverter erhalten."); } - // 6) Slice-Logik: l = 2*(reg–reg)+6 = 6, dann slice(-l, -4) → 2 Bytes - $lModbus = 2 * ($reg - $reg) + 6; // = 6 - $numBytes = $lModbus - 4; // = 2 + // 6) Slice-Logik: l = 2*(reg–reg)+6 = 6 ⇒ 2 Bytes Daten + $lModbus = 6; + $numBytes = 2; if (strlen($response) < $lModbus) { - throw new Exception("Unerwartet kurze Antwort (weniger als {$lModbus} Bytes)."); + throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)."); } $dataBytes = substr($response, -$lModbus, $numBytes); if (strlen($dataBytes) < 2) { throw new Exception("Data-Segment enthält weniger als 2 Bytes."); } - return $dataBytes; } /** - * Berechnet CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) über Binärdaten. - * - * @param string $binaryData - * @return int + * CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) */ private function calculateCRC16Modbus(string $binaryData): int { @@ -253,10 +233,7 @@ class SofarWechselrichter extends IPSModule } /** - * Hilfsfunktion: Prüft, ob eine Variable mit gegebenem Ident existiert. - * - * @param string $ident - * @return bool + * Prüft, ob eine Variable mit Ident existiert */ private function VariableExists(string $ident): bool {