From 6ffada6d29824c88846626af4c0010e6198af800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Wed, 4 Jun 2025 14:41:00 +0200 Subject: [PATCH] kleine anpasssung an Modul (32 und 64 Bit sollten nun auch funktionieren --- SofarWechselrichter/form.json | 27 ++++++ SofarWechselrichter/module.php | 158 +++++++++++++++++++++++---------- 2 files changed, 140 insertions(+), 45 deletions(-) diff --git a/SofarWechselrichter/form.json b/SofarWechselrichter/form.json index 7294a43..c7c602b 100644 --- a/SofarWechselrichter/form.json +++ b/SofarWechselrichter/form.json @@ -71,6 +71,33 @@ { "caption": "LE", "value": "LE" } ] } + }, + { + "caption": "Bit-Länge", + "name": "BitLength", + "width": "80px", + "add": "16", + "edit": { + "type": "Select", + "options": [ + { "caption": "16 Bit", "value": "16" }, + { "caption": "32 Bit", "value": "32" }, + { "caption": "64 Bit", "value": "64" } + ] + } + }, + { + "caption": "Signed/Unsigned", + "name": "Signedness", + "width": "100px", + "add": "Unsigned", + "edit": { + "type": "Select", + "options": [ + { "caption": "Unsigned", "value": "Unsigned" }, + { "caption": "Signed", "value": "Signed" } + ] + } } ] } diff --git a/SofarWechselrichter/module.php b/SofarWechselrichter/module.php index 08eeeae..e289af9 100644 --- a/SofarWechselrichter/module.php +++ b/SofarWechselrichter/module.php @@ -4,9 +4,13 @@ declare(strict_types=1); /** * Sofar Wechselrichter Modul (IP-Symcon) * - * - LoggerNumber wird als String gehalten, um Overflow zu vermeiden. - * - Negative Skalierungen sind erlaubt. - * - Beim Löschen eines Registers wird die zugehörige Variable automatisch entfernt. + * - LoggerNumber als String (kein Integer-Overflow). + * - Negative Skalierungen erlaubt. + * - Bit-Länge pro Register (16, 32, 64) auswählbar. + * - Signed/Unsigned pro Register wählbar. + * - Liest 32/64-Bit-Werte registerweise einzeln und setzt anschließend zusammen. + * - Gelöschte Register-Variablen werden entfernt. + * - Debug-Logs zeigen Raw-Response, um Slice-Position zu prüfen. */ class SofarWechselrichter extends IPSModule { @@ -38,7 +42,7 @@ class SofarWechselrichter extends IPSModule $registers = []; } - // 1) Variablen anlegen (neu hinzugekommene) + // 1) Variablen anlegen (neu hinzugekommene Register) $position = 10; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; @@ -53,22 +57,18 @@ class SofarWechselrichter extends IPSModule $position += 10; } - // 2) Variablen löschen (falls entfernt) - // Alle Kinder-IDs durchlaufen, Variablen mit Ident "Reg" entfernen, - // wenn sie nicht mehr in $registers stehen. + // 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) { - // Nur Variablen berücksichtigen $obj = IPS_GetObject($childID); if ($obj['ObjectType'] !== 2) { continue; } - $ident = $obj['ObjectIdent']; // VariableIdent + $ident = $obj['ObjectIdent']; if (substr($ident, 0, 3) === 'Reg') { if (!in_array($ident, $validIdents)) { IPS_DeleteVariable($childID); @@ -83,7 +83,7 @@ class SofarWechselrichter extends IPSModule } /** - * Wird aufgerufen durch IPS_RequestAction über Timer + * Wird aufgerufen durch IPS_RequestAction über den Timer */ public function RequestAction($Ident, $Value) { @@ -91,7 +91,6 @@ class SofarWechselrichter extends IPSModule case 'Query': $this->Query(); break; - default: throw new Exception('Ungültiger Ident: ' . $Ident); } @@ -102,6 +101,8 @@ class SofarWechselrichter extends IPSModule */ private function Query(): void { + $this->LogMessage('Query invoked', KL_MESSAGE); + $ip = trim($this->ReadPropertyString('IPAddress')); $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber')); @@ -123,57 +124,80 @@ class SofarWechselrichter extends IPSModule return; } - // 4) Für jedes Register: auslesen, skalieren, in Variable schreiben + // 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'])); - $ident = 'Reg' . $regNo; + $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 === '') { + if ($regNo < 0 || $label === '' || !in_array($bitLength, [16, 32, 64])) { continue; } try { - $bytes = $this->readRegister($ip, $loggerNumberStr, $regNo); - // Wert als UINT16 (BE oder LE) - if ($endian === 'LE') { - $arr = unpack('vvalue', $bytes); // Little-Endian - } else { - $arr = unpack('nvalue', $bytes); // Big-Endian + $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; } - $valueRaw = $arr['value']; - // bc* für sichere Multiplikation auch bei großen Zahlen / negativer Skalierung - $value = bcmul((string)$valueRaw, (string)$scale, 4); - SetValueFloat($this->GetIDForIdent($ident), (float)$value); + // 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}: " . $e->getMessage(), KL_WARNING); + $this->LogMessage( + "Fehler Lesen Reg {$regNo} ({$bitLength}bit, {$signedness}): " . $e->getMessage(), + KL_WARNING + ); } } } /** - * Liest ein einzelnes Register per Modbus-ähnlichem TCP (2 Bytes zurück) + * 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 + * @return string 2-Byte-Binär-String * @throws Exception Bei Kommunikationsfehlern */ - private function readRegister( - string $ip, - string $serial_nr_str, - int $reg - ): string + 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); - // Byte-Little-Endian: jeweils 2 Zeichen umkehren $hexSNbytes = [ substr($hexSN8, 6, 2), substr($hexSN8, 4, 2), @@ -190,7 +214,7 @@ class SofarWechselrichter extends IPSModule $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); $oFrame .= '0103' . $startHex . $numHex; - // 2) CRC16-Modbus über letzte 6 Bytes + // 2) CRC16-Modbus (letzte 6 Bytes) $crcInputHex = substr($oFrame, -12); $crcInputBin = hex2bin($crcInputHex); if ($crcInputBin === false) { @@ -201,8 +225,8 @@ class SofarWechselrichter extends IPSModule $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; + // 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); @@ -244,16 +268,23 @@ class SofarWechselrichter extends IPSModule throw new Exception("Keine Antwort vom Inverter erhalten."); } - // 6) Slice-Logik: l = 6 ⇒ 2 Bytes Daten - $lModbus = 6; + // 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) < 2) { - throw new Exception("Data-Segment enthält weniger als 2 Bytes."); + 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; } @@ -279,6 +310,43 @@ class SofarWechselrichter extends IPSModule 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. */