RegisterPropertyString('IPAddress', ''); $this->RegisterPropertyString('LoggerNumber', '0'); // als String $this->RegisterPropertyInteger('PollInterval', 60); $this->RegisterPropertyString('Registers', '[]'); // JSON-String // Timer für zyklische Abfragen (per RequestAction("Query")) $script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");'; $this->RegisterTimer('QueryTimer', 0, $script); } public function ApplyChanges() { parent::ApplyChanges(); // Timer-Intervall (ms) $intervalSec = $this->ReadPropertyInteger('PollInterval'); $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; $this->SetTimerInterval('QueryTimer', $intervalMs); // Aktuelle Registerliste $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { $registers = []; } // 1) Variablen anlegen (neu hinzugekommene Register) $position = 10; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim($entry['Label']); if ($regNo < 0 || $label === '') { continue; } $ident = 'Reg' . $regNo; if (!$this->VariableExists($ident)) { $this->RegisterVariableFloat($ident, $label, '', $position); } $position += 10; } // 2) Variablen löschen (falls Register entfernt wurden) $validIdents = []; foreach ($registers as $entry) { $validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']); } $children = IPS_GetChildrenIDs($this->InstanceID); foreach ($children as $childID) { $obj = IPS_GetObject($childID); if ($obj['ObjectType'] !== 2) { continue; } $ident = $obj['ObjectIdent']; if (substr($ident, 0, 3) === 'Reg') { if (!in_array($ident, $validIdents)) { IPS_DeleteVariable($childID); } } } } public function Destroy() { parent::Destroy(); } /** * Wird aufgerufen durch IPS_RequestAction über den Timer */ public function RequestAction($Ident, $Value) { switch ($Ident) { case 'Query': $this->Query(); break; default: throw new Exception('Ungültiger Ident: ' . $Ident); } } /** * Zyklische Abfrage aller definierten Register */ private function Query(): void { $this->LogMessage('Query invoked', KL_MESSAGE); $ip = trim($this->ReadPropertyString('IPAddress')); $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber')); // 1) Validierung IP if (!filter_var($ip, FILTER_VALIDATE_IP)) { $this->LogMessage('Abbruch: Ungültige IP = "' . $ip . '"', KL_WARNING); return; } // 2) Validierung LoggerNumber (Dezimal-String, > 0) if ($loggerNumberStr === '' || !ctype_digit($loggerNumberStr) || bccomp($loggerNumberStr, '1') < 0) { $this->LogMessage('Abbruch: Ungültige LoggerNumber = "' . $loggerNumberStr . '"', KL_WARNING); return; } // 3) Register-Liste einlesen $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers) || count($registers) === 0) { // Keine Register definiert return; } // 4) Für jedes Register: einzeln auslesen, zusammensetzen, skalieren, speichern foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim((string)$entry['Label']); $scale = (string) $entry['ScalingFactor']; // kann negativ sein $endian = strtoupper(trim((string)$entry['Endian'])); $bitLength = (int) $entry['BitLength']; // 16, 32 oder 64 $signedness = trim((string)$entry['Signedness']); // "Signed" oder "Unsigned" $ident = 'Reg' . $regNo; if ($regNo < 0 || $label === '' || !in_array($bitLength, [16, 32, 64])) { continue; } try { $numRegs = $bitLength / 16; // 1, 2 oder 4 // Bytes registerweise einzeln abfragen und zusammenfügen: $dataBytes = ''; for ($i = 0; $i < $numRegs; $i++) { $chunk = $this->readSingleRegister($ip, $loggerNumberStr, $regNo + $i); $dataBytes .= $chunk; } // Debug: raw combined response hex $combinedHex = strtoupper(bin2hex($dataBytes)); $this->LogMessage("Raw data for Reg {$regNo} ({$bitLength}bit): {$combinedHex}", KL_MESSAGE); // Endian-Handling: falls LE, kehre gesamte Byte-Reihenfolge um if ($endian === 'LE') { $combinedHex = $this->reverseByteOrder($combinedHex); $this->LogMessage("After LE reverse: {$combinedHex}", KL_MESSAGE); } // Konvertiere Hex in Dezimal-String $rawDec = $this->hexToDecimal($combinedHex); // Bei "Signed" → Zwei-Komplement-Umrechnung if ($signedness === 'Signed') { $half = bcpow('2', (string)($bitLength - 1), 0); // 2^(bitLength-1) $fullRange = bcpow('2', (string)$bitLength, 0); // 2^bitLength if (bccomp($rawDec, $half) >= 0) { $rawDec = bcsub($rawDec, $fullRange, 0); } $this->LogMessage("Signed rawDec for Reg {$regNo}: {$rawDec}", KL_MESSAGE); } // Skaliere (bc*-Multiplikation, 4 Nachkommastellen) $valueStr = bcmul($rawDec, $scale, 4); SetValueFloat($this->GetIDForIdent($ident), (float)$valueStr); $this->LogMessage("Final value for Reg {$regNo}: {$valueStr}", KL_MESSAGE); } catch (Exception $e) { $this->LogMessage( "Fehler Lesen Reg {$regNo} ({$bitLength}bit, {$signedness}): " . $e->getMessage(), KL_WARNING ); } } } /** * Liest genau ein Register (16 Bit) per Modbus-ähnlichem TCP (2 Bytes zurück). * * @param string $ip Inverter-IP * @param string $serial_nr_str Logger-Seriennummer als Dezimal-String * @param int $reg Register-Adresse * @return string 2-Byte-Binär-String * @throws Exception Bei Kommunikationsfehlern */ private function readSingleRegister(string $ip, string $serial_nr_str, int $reg): string { // 1) Out_Frame ohne CRC aufbauen $oFrame = 'a5170010450000'; // Dezimal-String → 8-stellige Hex $hexSN8 = $this->decStringToHex8($serial_nr_str); $hexSNbytes = [ substr($hexSN8, 6, 2), substr($hexSN8, 4, 2), substr($hexSN8, 2, 2), substr($hexSN8, 0, 2), ]; $oFrame .= implode('', $hexSNbytes); // Data-Field (16 Hex-Zeichen konstant) $oFrame .= '020000000000000000000000000000'; // Business-Field: 01 03 + Start-Register + Anzahl Register (1) $startHex = str_pad(dechex($reg), 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) $crcInputHex = substr($oFrame, -12); $crcInputBin = hex2bin($crcInputHex); 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)); $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; $bArr = []; for ($i = 0; $i < $l; $i++) { $byteHex = substr($oFrameWithCRC, 2 * $i, 2); $bArr[$i] = hexdec($byteHex); } $crcSum = 0; for ($i = 1; $i < $l; $i++) { $crcSum += $bArr[$i]; $crcSum &= 0xFF; } $bArr[$l] = $crcSum; $bArr[$l+1] = 0x15; $frameBin = ''; foreach ($bArr as $b) { $frameBin .= chr($b); } // 4) TCP-Verbindung öffnen & Paket senden (Port fest 8899) $port = 8899; $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, 5); if (!$fp) { throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})"); } fwrite($fp, $frameBin); stream_set_timeout($fp, 2); // 5) Antwort einlesen $response = ''; while (!feof($fp)) { $chunk = fread($fp, 1024); if ($chunk === false || $chunk === '') { break; } $response .= $chunk; } fclose($fp); if ($response === '') { throw new Exception("Keine Antwort vom Inverter erhalten."); } // Debug: log raw response hex $respHex = strtoupper(bin2hex($response)); $this->LogMessage("Raw response for single reg {$reg}: {$respHex}", KL_MESSAGE); // 6) Slice-Logik: l = 2*1 + 4 = 6, slice(-6, 2) → 2 Bytes $lModbus = 2 * 1 + 4; // = 6 $numBytes = 2; if (strlen($response) < $lModbus) { throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)."); } $dataBytes = substr($response, -$lModbus, $numBytes); if (strlen($dataBytes) < $numBytes) { throw new Exception("Data-Segment enthält weniger als {$numBytes} Bytes."); } $dataHex = strtoupper(bin2hex($dataBytes)); $this->LogMessage("Sliced data for single reg {$reg}: {$dataHex}", KL_MESSAGE); return $dataBytes; } /** * Wandelt einen Dezimal-String in einen 8-stelligen Hex-String um. * * @param string $decString * @return string 8-stellige Hex (uppercase) */ private function decStringToHex8(string $decString): string { $num = ltrim($decString, '0'); if ($num === '') { return '00000000'; } $hex = ''; while (bccomp($num, '0') > 0) { $mod = bcmod($num, '16'); $digit = dechex((int)$mod); $hex = strtoupper($digit) . $hex; $num = bcdiv($num, '16', 0); } return str_pad($hex, 8, '0', STR_PAD_LEFT); } /** * Kehrt die Byte-Reihenfolge eines Hex-Strings um (2 Hex-Zeichen = 1 Byte). * * @param string $hex Hex-Repr. (z.B. "A1B2C3D4") * @return string Umgekehrte Byte-Reihenfolge (z.B. "D4C3B2A1") */ private function reverseByteOrder(string $hex): string { $bytes = str_split($hex, 2); $bytes = array_reverse($bytes); return implode('', $bytes); } /** * Konvertiert einen Hex-String in einen Dezimal-String (BCMath). * * @param string $hex Uppercase-Hex ohne Präfix (z.B. "00FF10A3") * @return string Dezimal-String (z.B. "16737763") */ private function hexToDecimal(string $hex): string { $hex = ltrim($hex, '0'); if ($hex === '') { return '0'; } $len = strlen($hex); $dec = '0'; $power16 = '1'; for ($i = $len - 1; $i >= 0; $i--) { $digit = hexdec($hex[$i]); $term = bcmul((string)$digit, $power16, 0); $dec = bcadd($dec, $term, 0); $power16 = bcmul($power16, '16', 0); } return $dec; } /** * Berechnet CRC16-Modbus (Init=0xFFFF, Poly=0xA001) über Binärdaten. */ private function calculateCRC16Modbus(string $binaryData): int { $crc = 0xFFFF; $len = strlen($binaryData); for ($pos = 0; $pos < $len; $pos++) { $crc ^= ord($binaryData[$pos]); for ($i = 0; $i < 8; $i++) { if (($crc & 0x0001) !== 0) { $crc >>= 1; $crc ^= 0xA001; } else { $crc >>= 1; } } } return $crc; } /** * Prüft, ob eine Variable mit Ident existiert. */ private function VariableExists(string $ident): bool { $vid = @IPS_GetObjectIDByIdent($ident, $this->InstanceID); return ($vid !== false && IPS_VariableExists($vid)); } }