kleine anpasssung an Modul (32 und 64 Bit sollten nun auch funktionieren

This commit is contained in:
2025-06-04 14:41:00 +02:00
parent fa3232c736
commit 6ffada6d29
2 changed files with 140 additions and 45 deletions

View File

@@ -71,6 +71,33 @@
{ "caption": "LE", "value": "LE" } { "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" }
]
}
} }
] ]
} }

View File

@@ -4,9 +4,13 @@ declare(strict_types=1);
/** /**
* Sofar Wechselrichter Modul (IP-Symcon) * Sofar Wechselrichter Modul (IP-Symcon)
* *
* - LoggerNumber wird als String gehalten, um Overflow zu vermeiden. * - LoggerNumber als String (kein Integer-Overflow).
* - Negative Skalierungen sind erlaubt. * - Negative Skalierungen erlaubt.
* - Beim Löschen eines Registers wird die zugehörige Variable automatisch entfernt. * - 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 class SofarWechselrichter extends IPSModule
{ {
@@ -38,7 +42,7 @@ class SofarWechselrichter extends IPSModule
$registers = []; $registers = [];
} }
// 1) Variablen anlegen (neu hinzugekommene) // 1) Variablen anlegen (neu hinzugekommene Register)
$position = 10; $position = 10;
foreach ($registers as $entry) { foreach ($registers as $entry) {
$regNo = (int) $entry['RegisterNumber']; $regNo = (int) $entry['RegisterNumber'];
@@ -53,22 +57,18 @@ class SofarWechselrichter extends IPSModule
$position += 10; $position += 10;
} }
// 2) Variablen löschen (falls entfernt) // 2) Variablen löschen (falls Register entfernt wurden)
// Alle Kinder-IDs durchlaufen, Variablen mit Ident "Reg<Zahl>" entfernen,
// wenn sie nicht mehr in $registers stehen.
$validIdents = []; $validIdents = [];
foreach ($registers as $entry) { foreach ($registers as $entry) {
$validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']); $validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']);
} }
$children = IPS_GetChildrenIDs($this->InstanceID); $children = IPS_GetChildrenIDs($this->InstanceID);
foreach ($children as $childID) { foreach ($children as $childID) {
// Nur Variablen berücksichtigen
$obj = IPS_GetObject($childID); $obj = IPS_GetObject($childID);
if ($obj['ObjectType'] !== 2) { if ($obj['ObjectType'] !== 2) {
continue; continue;
} }
$ident = $obj['ObjectIdent']; // VariableIdent $ident = $obj['ObjectIdent'];
if (substr($ident, 0, 3) === 'Reg') { if (substr($ident, 0, 3) === 'Reg') {
if (!in_array($ident, $validIdents)) { if (!in_array($ident, $validIdents)) {
IPS_DeleteVariable($childID); 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) public function RequestAction($Ident, $Value)
{ {
@@ -91,7 +91,6 @@ class SofarWechselrichter extends IPSModule
case 'Query': case 'Query':
$this->Query(); $this->Query();
break; break;
default: default:
throw new Exception('Ungültiger Ident: ' . $Ident); throw new Exception('Ungültiger Ident: ' . $Ident);
} }
@@ -102,6 +101,8 @@ class SofarWechselrichter extends IPSModule
*/ */
private function Query(): void private function Query(): void
{ {
$this->LogMessage('Query invoked', KL_MESSAGE);
$ip = trim($this->ReadPropertyString('IPAddress')); $ip = trim($this->ReadPropertyString('IPAddress'));
$loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber')); $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber'));
@@ -123,57 +124,80 @@ class SofarWechselrichter extends IPSModule
return; 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) { foreach ($registers as $entry) {
$regNo = (int) $entry['RegisterNumber']; $regNo = (int) $entry['RegisterNumber'];
$label = trim((string)$entry['Label']); $label = trim((string)$entry['Label']);
$scale = (string) $entry['ScalingFactor']; // kann negativ sein $scale = (string) $entry['ScalingFactor']; // kann negativ sein
$endian = strtoupper(trim((string)$entry['Endian'])); $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; $ident = 'Reg' . $regNo;
if ($regNo < 0 || $label === '') { if ($regNo < 0 || $label === '' || !in_array($bitLength, [16, 32, 64])) {
continue; continue;
} }
try { try {
$bytes = $this->readRegister($ip, $loggerNumberStr, $regNo); $numRegs = $bitLength / 16; // 1, 2 oder 4
// Wert als UINT16 (BE oder LE) // Bytes registerweise einzeln abfragen und zusammenfügen:
if ($endian === 'LE') { $dataBytes = '';
$arr = unpack('vvalue', $bytes); // Little-Endian for ($i = 0; $i < $numRegs; $i++) {
} else { $chunk = $this->readSingleRegister($ip, $loggerNumberStr, $regNo + $i);
$arr = unpack('nvalue', $bytes); // Big-Endian $dataBytes .= $chunk;
} }
$valueRaw = $arr['value']; // Debug: raw combined response hex
// bc* für sichere Multiplikation auch bei großen Zahlen / negativer Skalierung $combinedHex = strtoupper(bin2hex($dataBytes));
$value = bcmul((string)$valueRaw, (string)$scale, 4); $this->LogMessage("Raw data for Reg {$regNo} ({$bitLength}bit): {$combinedHex}", KL_MESSAGE);
SetValueFloat($this->GetIDForIdent($ident), (float)$value);
// 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) { } 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 $ip Inverter-IP
* @param string $serial_nr_str Logger-Seriennummer als Dezimal-String * @param string $serial_nr_str Logger-Seriennummer als Dezimal-String
* @param int $reg Register-Adresse * @param int $reg Register-Adresse
* @return string 2 Byte binär * @return string 2-Byte-Binär-String
* @throws Exception Bei Kommunikationsfehlern * @throws Exception Bei Kommunikationsfehlern
*/ */
private function readRegister( private function readSingleRegister(string $ip, string $serial_nr_str, int $reg): string
string $ip,
string $serial_nr_str,
int $reg
): string
{ {
// 1) Out_Frame ohne CRC aufbauen // 1) Out_Frame ohne CRC aufbauen
$oFrame = 'a5170010450000'; $oFrame = 'a5170010450000';
// Dezimal-String → 8-stellige Hex // Dezimal-String → 8-stellige Hex
$hexSN8 = $this->decStringToHex8($serial_nr_str); $hexSN8 = $this->decStringToHex8($serial_nr_str);
// Byte-Little-Endian: jeweils 2 Zeichen umkehren
$hexSNbytes = [ $hexSNbytes = [
substr($hexSN8, 6, 2), substr($hexSN8, 6, 2),
substr($hexSN8, 4, 2), substr($hexSN8, 4, 2),
@@ -190,7 +214,7 @@ class SofarWechselrichter extends IPSModule
$numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT);
$oFrame .= '0103' . $startHex . $numHex; $oFrame .= '0103' . $startHex . $numHex;
// 2) CRC16-Modbus über letzte 6 Bytes // 2) CRC16-Modbus (letzte 6 Bytes)
$crcInputHex = substr($oFrame, -12); $crcInputHex = substr($oFrame, -12);
$crcInputBin = hex2bin($crcInputHex); $crcInputBin = hex2bin($crcInputHex);
if ($crcInputBin === false) { if ($crcInputBin === false) {
@@ -201,7 +225,7 @@ class SofarWechselrichter extends IPSModule
$crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2); $crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2);
$oFrameWithCRC = $oFrame . strtolower($crcSwapped); $oFrameWithCRC = $oFrame . strtolower($crcSwapped);
// 3) Summen-Checksum (Bytes ab Index 1) + 0x15 // 3) Summen-Checksum (alle Bytes ab Index 1) + 0x15
$l = strlen($oFrameWithCRC) / 2; $l = strlen($oFrameWithCRC) / 2;
$bArr = []; $bArr = [];
for ($i = 0; $i < $l; $i++) { for ($i = 0; $i < $l; $i++) {
@@ -244,16 +268,23 @@ class SofarWechselrichter extends IPSModule
throw new Exception("Keine Antwort vom Inverter erhalten."); throw new Exception("Keine Antwort vom Inverter erhalten.");
} }
// 6) Slice-Logik: l = 6 ⇒ 2 Bytes Daten // Debug: log raw response hex
$lModbus = 6; $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; $numBytes = 2;
if (strlen($response) < $lModbus) { if (strlen($response) < $lModbus) {
throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)."); throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes).");
} }
$dataBytes = substr($response, -$lModbus, $numBytes); $dataBytes = substr($response, -$lModbus, $numBytes);
if (strlen($dataBytes) < 2) { if (strlen($dataBytes) < $numBytes) {
throw new Exception("Data-Segment enthält weniger als 2 Bytes."); 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; return $dataBytes;
} }
@@ -279,6 +310,43 @@ class SofarWechselrichter extends IPSModule
return str_pad($hex, 8, '0', STR_PAD_LEFT); 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. * Berechnet CRC16-Modbus (Init=0xFFFF, Poly=0xA001) über Binärdaten.
*/ */