no message

This commit is contained in:
dh
2025-06-04 10:49:28 +02:00
parent e5312ed15f
commit ed8a07586d
2 changed files with 136 additions and 86 deletions
+26 -2
View File
@@ -4,10 +4,21 @@
"type": "Label", "type": "Label",
"caption": "Sofar Wechselrichter Konfiguration" "caption": "Sofar Wechselrichter Konfiguration"
}, },
{
"type": "ValidationTextBox",
"name": "IPAddress",
"caption": "Inverter IP-Adresse",
"validate": {
"required": true,
"pattern": "^((25[0-5]|2[0-4]\\d|[01]?\\d?\\d)(\\.(?!$)|$)){4}$",
"errorMessage": "Bitte eine gültige IPv4-Adresse eingeben."
}
},
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
"name": "LoggerNumber", "name": "LoggerNumber",
"caption": "Logger-Seriennummer" "caption": "Logger-Seriennummer",
"minimum": 0
}, },
{ {
"type": "NumberSpinner", "type": "NumberSpinner",
@@ -20,7 +31,7 @@
"type": "List", "type": "List",
"name": "Registers", "name": "Registers",
"caption": "Register-Tabelle", "caption": "Register-Tabelle",
"add": "Registriere neuen Eintrag", "add": "Neues Register hinzufügen",
"delete": "Lösche Eintrag", "delete": "Lösche Eintrag",
"columns": [ "columns": [
{ {
@@ -52,6 +63,19 @@
"digits": 4, "digits": 4,
"minimum": 0 "minimum": 0
} }
},
{
"caption": "Endian",
"name": "Endian",
"width": "80px",
"add": "BE",
"edit": {
"type": "Select",
"options": [
{ "caption": "BE", "value": "BE" },
{ "caption": "LE", "value": "LE" }
]
}
} }
] ]
} }
+102 -76
View File
@@ -5,15 +5,14 @@ class SofarWechselrichter extends IPSModule
{ {
public function Create() public function Create()
{ {
// Muss immer zuerst aufgerufen werden
parent::Create(); parent::Create();
// Moduleigenschaften
// Eigenschaften (Properties) anlegen $this->RegisterPropertyString('IPAddress', '172.31.70.80');
$this->RegisterPropertyInteger('LoggerNumber', 0); $this->RegisterPropertyInteger('LoggerNumber', 0);
$this->RegisterPropertyInteger('PollInterval', 60); // Standard 60 Sekunden $this->RegisterPropertyInteger('PollInterval', 60);
$this->RegisterPropertyString('Registers', '[]'); // JSON-String für Matrix $this->RegisterPropertyString('Registers', '[]'); // JSON-String
// Timer für zyklisches Abfragen registrieren (wird in ApplyChanges aktiviert) // Timer für zyklische Abfragen
$this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);'); $this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);');
} }
@@ -21,34 +20,31 @@ class SofarWechselrichter extends IPSModule
{ {
parent::ApplyChanges(); parent::ApplyChanges();
// PollInterval in Millisekunden umrechnen // Timer-Intervall (ms) setzen
$intervalSec = $this->ReadPropertyInteger('PollInterval'); $intervalSec = $this->ReadPropertyInteger('PollInterval');
$intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0;
$this->SetTimerInterval('QueryTimer', $intervalMs); $this->SetTimerInterval('QueryTimer', $intervalMs);
// Variablen für "Vorheriger Wert" (Register 1160) registrieren // Variable für vorherigen Wert (Register 1160) anlegen, falls noch nicht vorhanden
$this->RegisterVariableInteger('PrevValue1160', 'Vorheriger Wert', '', 10); if (!$this->VariableExists('PrevValue1160')) {
$this->RegisterVariableInteger('PrevValue1160', 'Vorheriger Wert (Reg 1160)', '', 10);
}
// Aus der Property "Registers" JSON-String holen und in Array Decodieren // Alle Einträge aus „Registers“-Liste aus Property lesen
$registers = json_decode($this->ReadPropertyString('Registers'), true); $registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers)) { if (!is_array($registers)) {
$registers = []; $registers = [];
} }
// Für jeden definierten Register-Eintrag: Variable anlegen, falls nicht vorhanden // Für jeden Eintrag: Variable anlegen, falls nicht existiert
$position = 20; $position = 20;
foreach ($registers as $entry) { foreach ($registers as $entry) {
$regNo = (int) $entry['RegisterNumber']; $regNo = (int) $entry['RegisterNumber'];
$label = trim($entry['Label']); $label = trim($entry['Label']);
// Ident darf keine Sonderzeichen/Leerzeichen enthalten – wir verwenden "Reg<RegNummer>"
$ident = 'Reg' . $regNo;
// Falls Bezeichnung leer oder Duplicate, überspringen
if ($regNo < 0 || $label === '') { if ($regNo < 0 || $label === '') {
continue; continue;
} }
$ident = 'Reg' . $regNo;
// Existiert die Variable noch nicht? Dann anlegen
if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) { if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) {
$this->RegisterVariableFloat($ident, $label, '', $position); $this->RegisterVariableFloat($ident, $label, '', $position);
} }
@@ -56,35 +52,40 @@ class SofarWechselrichter extends IPSModule
} }
} }
public function Destroy()
{
parent::Destroy();
}
/** /**
* Timer-Callback: liest zyklisch alle definierten Register. * Timer-Callback: wird in Intervallen aufgerufen
*/ */
public function Query(): void public function Query(): void
{ {
$ip = $this->ReadPropertyString('IPAddress');
$loggerNumber = $this->ReadPropertyInteger('LoggerNumber'); $loggerNumber = $this->ReadPropertyInteger('LoggerNumber');
// Ohne gültige Seriennummer abbrechen
if ($loggerNumber <= 0) { if ($loggerNumber <= 0) {
// Ohne gültige Seriennummer bringt eine Abfrage keinen Sinn $this->LogMessage('LoggerNumber ist ≤ 0, Abbruch', KL_WARNING);
return; return;
} }
// 1) Vorherigen Wert (Register 1160) als INT16BE auslesen // (1) Register 1160 als INT16BE auslesen → „Vorheriger Wert“
try { try {
$bytes1160 = $this->readRegister($loggerNumber, 1160); $bytes1160 = $this->readRegister($ip, $loggerNumber, 1160, 'BE');
// 'n' = unsigned 16-bit big endian
$arr1160 = unpack('nvalue', $bytes1160); $arr1160 = unpack('nvalue', $bytes1160);
$raw1160 = $arr1160['value']; $raw1160 = $arr1160['value'];
// Vorzeichenkorrektur für INT16 // Vorzeichenkorrektur
if ($raw1160 & 0x8000) { if ($raw1160 & 0x8000) {
$raw1160 -= 0x10000; $raw1160 -= 0x10000;
} }
// Speichere in Variable "PrevValue1160"
SetValue($this->GetIDForIdent('PrevValue1160'), $raw1160); SetValue($this->GetIDForIdent('PrevValue1160'), $raw1160);
} catch (Exception $e) { } catch (Exception $e) {
// Im Fehlerfall einfach in die Debug-Konsole schreiben $this->LogMessage('Fehler Lesen Reg 1160: ' . $e->getMessage(), KL_WARNING);
$this->LogMessage('Fehler beim Lesen von Register 1160: ' . $e->getMessage(), KL_WARNING);
} }
// 2) Tabelle auslesen und alle Einträge abfragen // (2) Alle Einträge aus der Registerliste abfragen
$registers = json_decode($this->ReadPropertyString('Registers'), true); $registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers)) { if (!is_array($registers)) {
return; return;
@@ -92,8 +93,9 @@ class SofarWechselrichter extends IPSModule
foreach ($registers as $entry) { foreach ($registers as $entry) {
$regNo = (int) $entry['RegisterNumber']; $regNo = (int) $entry['RegisterNumber'];
$label = trim($entry['Label']); $label = trim((string)$entry['Label']);
$scale = (float) $entry['ScalingFactor']; $scale = (float) $entry['ScalingFactor'];
$endian = strtoupper(trim((string)$entry['Endian']));
$ident = 'Reg' . $regNo; $ident = 'Reg' . $regNo;
if ($regNo < 0 || $label === '') { if ($regNo < 0 || $label === '') {
@@ -101,34 +103,42 @@ class SofarWechselrichter extends IPSModule
} }
try { try {
$bytes = $this->readRegister($loggerNumber, $regNo); $bytes = $this->readRegister($ip, $loggerNumber, $regNo, $endian);
// 'n' = unsigned 16-bit big endian if ($endian === 'LE') {
$arr = unpack('nvalue', $bytes); $arr = unpack('vvalue', $bytes); // uint16 little-endian
} else {
$arr = unpack('nvalue', $bytes); // uint16 big-endian
}
$valueRaw = $arr['value']; $valueRaw = $arr['value'];
// Skaliere $value = $valueRaw * $scale;
$valueScaled = $valueRaw * $scale; SetValue($this->GetIDForIdent($ident), $value);
// Schreibe in die korrespondierende Float-Variable
SetValue($this->GetIDForIdent($ident), $valueScaled);
} catch (Exception $e) { } catch (Exception $e) {
$this->LogMessage("Fehler beim Lesen von Register {$regNo}: " . $e->getMessage(), KL_WARNING); $this->LogMessage("Fehler Lesen Reg {$regNo}: " . $e->getMessage(), KL_WARNING);
} }
} }
} }
/** /**
* Führt eine Modbus-ähnliche Abfrage an Sofar durch und gibt genau 2 Bytes zurück. * Liest genau 2 Bytes eines Registers (Modbus-ähnlich über TCP, Port fix 8899).
* *
* @param string $ip IP-Adresse des Inverters
* @param int $serial_nr Seriennummer (Logger-Nummer) * @param int $serial_nr Seriennummer (Logger-Nummer)
* @param int $reg Register-Adresse (dezimal, ein einziges Register) * @param int $reg Register-Adresse
* @return string Binäre Zeichenkette mit genau 2 Bytes (Registerinhalt) * @param string $endian 'BE' oder 'LE'
* @throws Exception Bei jeglichem Kommunikationsfehler * @return string Binärkette mit genau 2 Bytes
* @throws Exception Bei Kommunikationsfehlern
*/ */
private function readRegister(int $serial_nr, int $reg): string private function readRegister(
string $ip,
int $serial_nr,
int $reg,
string $endian
): string
{ {
// 1) Out_Frame ohne CRC erstellen // 1) Out_Frame ohne CRC aufbauen
$oFrame = 'a5170010450000'; $oFrame = 'a5170010450000';
// Seriennummer in 8-stelliges Hex (jekill Byte im Little-Endian) // Seriennummer (8-stelliges Hex), Byte-Little-Endian pro Byte
$hexSN = str_pad(dechex($serial_nr), 8, '0', STR_PAD_LEFT); $hexSN = str_pad(dechex($serial_nr), 8, '0', STR_PAD_LEFT);
$hexSNbytes = [ $hexSNbytes = [
substr($hexSN, 6, 2), substr($hexSN, 6, 2),
@@ -147,38 +157,19 @@ class SofarWechselrichter extends IPSModule
$numHex = str_pad(dechex($numRegs), 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 die letzten 6 Bytes (12 Hex-Zeichen) // 2) CRC16-Modbus (letzte 6 Bytes)
$crcInputHex = substr($oFrame, -12); $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); $crcInputBin = hex2bin($crcInputHex);
if ($crcInputBin === false) { if ($crcInputBin === false) {
throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}"); throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}");
} }
$crcValue = $this->calculateCRC16Modbus($crcInputBin);
$crcValue = $crc16($crcInputBin);
$crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT)); $crcHex = strtoupper(str_pad(dechex($crcValue), 4, '0', STR_PAD_LEFT));
// Byte-Swap (Low-Byte zuerst)
$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 über alle Bytes ab Index 1 + End-Byte 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++) {
@@ -198,17 +189,17 @@ class SofarWechselrichter extends IPSModule
$frameBin .= chr($b); $frameBin .= chr($b);
} }
// 4) TCP-Verbindung öffnen, Paket senden und Antwort einlesen // 4) TCP-Verbindung öffnen & Paket senden (Port ist fest auf 8899)
$ip = '172.31.70.80';
$port = 8899; $port = 8899;
$timeout = 5; $timeoutSec = 5;
$fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeout); $fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeoutSec);
if (!$fp) { if (!$fp) {
throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})"); throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})");
} }
fwrite($fp, $frameBin); fwrite($fp, $frameBin);
stream_set_timeout($fp, 2); stream_set_timeout($fp, 2);
// 5) Antwort einlesen
$response = ''; $response = '';
while (!feof($fp)) { while (!feof($fp)) {
$chunk = fread($fp, 1024); $chunk = fread($fp, 1024);
@@ -220,21 +211,56 @@ class SofarWechselrichter extends IPSModule
fclose($fp); fclose($fp);
if ($response === '') { if ($response === '') {
throw new Exception("Keine Antwort vom Inverter."); throw new Exception("Keine Antwort vom Inverter erhalten.");
} }
// 5) Slice: Node-RED: l = 2*(reg–reg)+6 = 6. Dann slice(-l, -4) → (l–4) = 2 Bytes // 6) Slice-Logik: l = 2*(reg–reg)+6 = 6, dann slice(-l, -4) → 2 Bytes
$lModbus = 2 * ($reg - $reg) + 6; // = 6 $lModbus = 2 * ($reg - $reg) + 6; // = 6
$numBytes = $lModbus - 4; // = 2 $numBytes = $lModbus - 4; // = 2
if (strlen($response) < $lModbus) { if (strlen($response) < $lModbus) {
throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes)"); throw new Exception("Unerwartet kurze Antwort (weniger als {$lModbus} Bytes).");
} }
// Start vom Ende: -(lModbus), Länge = 2
$dataBytes = substr($response, -$lModbus, $numBytes); $dataBytes = substr($response, -$lModbus, $numBytes);
if (strlen($dataBytes) < 2) { if (strlen($dataBytes) < 2) {
throw new Exception("Data-Segment < 2 Bytes."); throw new Exception("Data-Segment enthält weniger als 2 Bytes.");
} }
return $dataBytes; return $dataBytes;
} }
/**
* Berechnet CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) über Binärdaten.
*
* @param string $binaryData
* @return int
*/
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;
}
/**
* Hilfsfunktion: Prüft, ob eine Variable mit gegebenem Ident existiert.
*
* @param string $ident
* @return bool
*/
private function VariableExists(string $ident): bool
{
$vid = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
return ($vid !== false && IPS_VariableExists($vid));
}
} }