RegisterPropertyInteger('LoggerNumber', 0); $this->RegisterPropertyInteger('PollInterval', 60); // Standard 60 Sekunden $this->RegisterPropertyString('Registers', '[]'); // JSON-String für Matrix // Timer für zyklisches Abfragen registrieren (wird in ApplyChanges aktiviert) $this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);'); } public function ApplyChanges() { parent::ApplyChanges(); // PollInterval in Millisekunden umrechnen $intervalSec = $this->ReadPropertyInteger('PollInterval'); $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; $this->SetTimerInterval('QueryTimer', $intervalMs); // Variablen für "Vorheriger Wert" (Register 1160) registrieren $this->RegisterVariableInteger('PrevValue1160', 'Vorheriger Wert', '', 10); // Aus der Property "Registers" JSON-String holen und in Array Decodieren $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { $registers = []; } // Für jeden definierten Register-Eintrag: Variable anlegen, falls nicht vorhanden $position = 20; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim($entry['Label']); // Ident darf keine Sonderzeichen/Leerzeichen enthalten – wir verwenden "Reg" $ident = 'Reg' . $regNo; // Falls Bezeichnung leer oder Duplicate, überspringen if ($regNo < 0 || $label === '') { continue; } // Existiert die Variable noch nicht? Dann anlegen if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) { $this->RegisterVariableFloat($ident, $label, '', $position); } $position += 10; } } /** * Timer-Callback: liest zyklisch alle definierten Register. */ public function Query(): void { $loggerNumber = $this->ReadPropertyInteger('LoggerNumber'); if ($loggerNumber <= 0) { // Ohne gültige Seriennummer bringt eine Abfrage keinen Sinn return; } // 1) Vorherigen Wert (Register 1160) als INT16BE auslesen try { $bytes1160 = $this->readRegister($loggerNumber, 1160); // 'n' = unsigned 16-bit big endian $arr1160 = unpack('nvalue', $bytes1160); $raw1160 = $arr1160['value']; // Vorzeichenkorrektur für INT16 if ($raw1160 & 0x8000) { $raw1160 -= 0x10000; } // Speichere in Variable "PrevValue1160" SetValue($this->GetIDForIdent('PrevValue1160'), $raw1160); } catch (Exception $e) { // Im Fehlerfall einfach in die Debug-Konsole schreiben $this->LogMessage('Fehler beim Lesen von Register 1160: ' . $e->getMessage(), KL_WARNING); } // 2) Tabelle auslesen und alle Einträge abfragen $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { return; } foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim($entry['Label']); $scale = (float) $entry['ScalingFactor']; $ident = 'Reg' . $regNo; if ($regNo < 0 || $label === '') { continue; } try { $bytes = $this->readRegister($loggerNumber, $regNo); // 'n' = unsigned 16-bit big endian $arr = unpack('nvalue', $bytes); $valueRaw = $arr['value']; // Skaliere $valueScaled = $valueRaw * $scale; // Schreibe in die korrespondierende Float-Variable SetValue($this->GetIDForIdent($ident), $valueScaled); } catch (Exception $e) { $this->LogMessage("Fehler beim Lesen von Register {$regNo}: " . $e->getMessage(), KL_WARNING); } } } /** * Führt eine Modbus-ähnliche Abfrage an Sofar durch und gibt genau 2 Bytes zurück. * * @param int $serial_nr Seriennummer (Logger-Nummer) * @param int $reg Register-Adresse (dezimal, ein einziges Register) * @return string Binäre Zeichenkette mit genau 2 Bytes (Registerinhalt) * @throws Exception Bei jeglichem Kommunikationsfehler */ private function readRegister(int $serial_nr, int $reg): string { // 1) Out_Frame ohne CRC erstellen $oFrame = 'a5170010450000'; // Seriennummer in 8-stelliges Hex (jekill Byte im Little-Endian) $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); // 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); $oFrame .= '0103' . $startHex . $numHex; // 2) CRC16-Modbus über die letzten 6 Bytes (12 Hex-Zeichen) $crcInputHex = substr($oFrame, -12); // Hilfsfunktion: CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) $crc16 = function(string $bin): int { $crc = 0xFFFF; $len = strlen($bin); for ($pos = 0; $pos < $len; $pos++) { $crc ^= ord($bin[$pos]); for ($i = 0; $i < 8; $i++) { if (($crc & 0x0001) !== 0) { $crc >>= 1; $crc ^= 0xA001; } else { $crc >>= 1; } } } return $crc; }; $crcInputBin = hex2bin($crcInputHex); if ($crcInputBin === false) { throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}"); } $crcValue = $crc16($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 über alle Bytes ab Index 1 + End-Byte 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 und Antwort einlesen $ip = '172.31.70.80'; $port = 8899; $timeout = 5; $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeout); if (!$fp) { throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})"); } fwrite($fp, $frameBin); stream_set_timeout($fp, 2); $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."); } // 5) Slice: Node-RED: l = 2*(reg–reg)+6 = 6. Dann slice(-l, -4) → (l–4) = 2 Bytes $lModbus = 2 * ($reg - $reg) + 6; // = 6 $numBytes = $lModbus - 4; // = 2 if (strlen($response) < $lModbus) { throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)"); } // Start vom Ende: -(lModbus), Länge = 2 $dataBytes = substr($response, -$lModbus, $numBytes); if (strlen($dataBytes) < 2) { throw new Exception("Data-Segment < 2 Bytes."); } return $dataBytes; } }