mehrere arten von registern
This commit is contained in:
@@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ 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.
|
||||||
|
* - Gelöschte Register-Variablen werden entfernt.
|
||||||
*/
|
*/
|
||||||
class SofarWechselrichter extends IPSModule
|
class SofarWechselrichter extends IPSModule
|
||||||
{
|
{
|
||||||
@@ -14,7 +16,7 @@ class SofarWechselrichter extends IPSModule
|
|||||||
{
|
{
|
||||||
parent::Create();
|
parent::Create();
|
||||||
// Moduleigenschaften
|
// Moduleigenschaften
|
||||||
$this->RegisterPropertyString('IPAddress', '');
|
$this->RegisterPropertyString('IPAddress', '172.31.70.80');
|
||||||
$this->RegisterPropertyString('LoggerNumber', '0'); // als String
|
$this->RegisterPropertyString('LoggerNumber', '0'); // als String
|
||||||
$this->RegisterPropertyInteger('PollInterval', 60);
|
$this->RegisterPropertyInteger('PollInterval', 60);
|
||||||
$this->RegisterPropertyString('Registers', '[]'); // JSON-String
|
$this->RegisterPropertyString('Registers', '[]'); // JSON-String
|
||||||
@@ -38,7 +40,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 +55,19 @@ 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
|
// Nur Variablen berücksichtigen (ObjectType = 2)
|
||||||
$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 +82,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 +90,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);
|
||||||
}
|
}
|
||||||
@@ -123,49 +121,68 @@ class SofarWechselrichter extends IPSModule
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Für jedes Register: auslesen, skalieren, in Variable schreiben
|
// 4) Für jedes Register: auslesen, zusammenbauen, Skalierung, 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']));
|
||||||
$ident = 'Reg' . $regNo;
|
$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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$bytes = $this->readRegister($ip, $loggerNumberStr, $regNo);
|
$numRegs = $bitLength / 16; // 1, 2 oder 4
|
||||||
// Wert als UINT16 (BE oder LE)
|
$bytes = $this->readRegisters($ip, $loggerNumberStr, $regNo, $numRegs);
|
||||||
|
// bytes enthält dann (2 * $numRegs) Bytes
|
||||||
|
$hex = strtoupper(bin2hex($bytes));
|
||||||
|
// Endian-Handling: falls LE, kehre gesamte Byte-Reihenfolge um
|
||||||
if ($endian === 'LE') {
|
if ($endian === 'LE') {
|
||||||
$arr = unpack('vvalue', $bytes); // Little-Endian
|
$hex = $this->reverseByteOrder($hex);
|
||||||
} else {
|
|
||||||
$arr = unpack('nvalue', $bytes); // Big-Endian
|
|
||||||
}
|
}
|
||||||
$valueRaw = $arr['value'];
|
// Konvertiere Hex in Dezimal-String
|
||||||
// bc* für sichere Multiplikation auch bei großen Zahlen / negativer Skalierung
|
$rawDec = $this->hexToDecimal($hex);
|
||||||
$value = bcmul((string)$valueRaw, (string)$scale, 4);
|
|
||||||
SetValueFloat($this->GetIDForIdent($ident), (float)$value);
|
// Bei "Signed" → Zwei-Komplement-Umrechnung
|
||||||
|
if ($signedness === 'Signed') {
|
||||||
|
// 2^(bitLength - 1)
|
||||||
|
$half = bcpow('2', (string)($bitLength - 1), 0);
|
||||||
|
// 2^bitLength
|
||||||
|
$fullRange = bcpow('2', (string)$bitLength, 0);
|
||||||
|
if (bccomp($rawDec, $half) >= 0) {
|
||||||
|
// rawDec - 2^bitLength
|
||||||
|
$rawDec = bcsub($rawDec, $fullRange, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skaliere (bc*-Multiplikation, 4 Nachkommastellen)
|
||||||
|
$valueStr = bcmul($rawDec, $scale, 4);
|
||||||
|
SetValueFloat($this->GetIDForIdent($ident), (float)$valueStr);
|
||||||
} 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 $numRegs aufeinanderfolgende Register per Modbus-ähnlichem TCP (2*$numRegs 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 Start-Register-Adresse
|
||||||
* @return string 2 Byte binär
|
* @param int $numRegs Anzahl aufeinanderfolgender Register (1..4)
|
||||||
|
* @return string Binär-String mit genau 2*$numRegs Bytes
|
||||||
* @throws Exception Bei Kommunikationsfehlern
|
* @throws Exception Bei Kommunikationsfehlern
|
||||||
*/
|
*/
|
||||||
private function readRegister(
|
private function readRegisters(
|
||||||
string $ip,
|
string $ip,
|
||||||
string $serial_nr_str,
|
string $serial_nr_str,
|
||||||
int $reg
|
int $reg,
|
||||||
|
int $numRegs
|
||||||
): string
|
): string
|
||||||
{
|
{
|
||||||
// 1) Out_Frame ohne CRC aufbauen
|
// 1) Out_Frame ohne CRC aufbauen
|
||||||
@@ -173,7 +190,6 @@ class SofarWechselrichter extends IPSModule
|
|||||||
|
|
||||||
// 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),
|
||||||
@@ -185,9 +201,9 @@ class SofarWechselrichter extends IPSModule
|
|||||||
// Data-Field (16 Hex-Zeichen konstant)
|
// Data-Field (16 Hex-Zeichen konstant)
|
||||||
$oFrame .= '020000000000000000000000000000';
|
$oFrame .= '020000000000000000000000000000';
|
||||||
|
|
||||||
// Business-Field: 01 03 + Start-Register + Anzahl Register (1)
|
// Business-Field: 01 03 + Start-Register + Anzahl Register ($numRegs)
|
||||||
$startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT);
|
$startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT);
|
||||||
$numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT);
|
$numHex = str_pad(dechex($numRegs), 4, '0', STR_PAD_LEFT);
|
||||||
$oFrame .= '0103' . $startHex . $numHex;
|
$oFrame .= '0103' . $startHex . $numHex;
|
||||||
|
|
||||||
// 2) CRC16-Modbus über letzte 6 Bytes
|
// 2) CRC16-Modbus über letzte 6 Bytes
|
||||||
@@ -244,15 +260,15 @@ 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
|
// 6) Slice-Logik: l = 2*$numRegs + 6, dann slice(-l, -4) liefert 2*$numRegs Bytes
|
||||||
$lModbus = 6;
|
$lModbus = 2 * $numRegs + 6;
|
||||||
$numBytes = 2;
|
$numBytes = 2 * $numRegs;
|
||||||
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.");
|
||||||
}
|
}
|
||||||
return $dataBytes;
|
return $dataBytes;
|
||||||
}
|
}
|
||||||
@@ -279,6 +295,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 HexRepr (z.B. "A1B2C3D4")
|
||||||
|
* @return string Umgekehrte ByteReihenfolge (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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user