RegisterPropertyString('IPAddress', '172.31.70.80'); $this->RegisterPropertyInteger('LoggerNumber', 0); $this->RegisterPropertyInteger('PollInterval', 60); $this->RegisterPropertyString('Registers', '[]'); // JSON-String // 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); // Variablen für jeden definierten Register-Eintrag anlegen $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { $registers = []; } $position = 10; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim($entry['Label']); if ($regNo < 0 || $label === '') { continue; } $ident = 'Reg' . $regNo; if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) { $this->RegisterVariableFloat($ident, $label, '', $position); } $position += 10; } } public function Destroy() { parent::Destroy(); } /** * Wird aufgerufen durch IPS_RequestAction über Timer */ 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'); if ($loggerNumber <= 0 || filter_var($ip, FILTER_VALIDATE_IP) === false) { $this->LogMessage( 'Abbruch: LoggerNumber = ' . $loggerNumber . ', IP = "' . $ip . '"', KL_WARNING ); return; } $registers = json_decode($this->ReadPropertyString('Registers'), true); 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; if ($regNo < 0 || $label === '') { continue; } try { $bytes = $this->readRegister($ip, $loggerNumber, $regNo, $endian); if ($endian === 'LE') { $arr = unpack('vvalue', $bytes); // UINT16 LE } else { $arr = unpack('nvalue', $bytes); // UINT16 BE } $valueRaw = $arr['value']; $value = $valueRaw * $scale; SetValue($this->GetIDForIdent($ident), $value); } catch (Exception $e) { $this->LogMessage("Fehler Lesen Reg {$regNo}: " . $e->getMessage(), KL_WARNING); } } } /** * Liest ein einzelnes Register per Modbus-ähnlichem TCP (2 Bytes zurück) * * @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, int $serial_nr, int $reg, string $endian ): string { // 1) Out_Frame ohne CRC aufbauen $oFrame = 'a5170010450000'; $hexSN = str_pad(dechex($serial_nr), 8, '0', STR_PAD_LEFT); $hexSNbytes = [ substr($hexSN, 6, 2), substr($hexSN, 4, 2), substr($hexSN, 2, 2), substr($hexSN, 0, 2), ]; $oFrame .= implode('', $hexSNbytes); $oFrame .= '020000000000000000000000000000'; $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 (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."); } // 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 (< {$lModbus} Bytes)."); } $dataBytes = substr($response, -$lModbus, $numBytes); if (strlen($dataBytes) < 2) { throw new Exception("Data-Segment enthält weniger als 2 Bytes."); } return $dataBytes; } /** * CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) */ 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)); } }