no message
This commit is contained in:
@@ -5,15 +5,14 @@ class SofarWechselrichter extends IPSModule
|
||||
{
|
||||
public function Create()
|
||||
{
|
||||
// Muss immer zuerst aufgerufen werden
|
||||
parent::Create();
|
||||
|
||||
// Eigenschaften (Properties) anlegen
|
||||
// Moduleigenschaften
|
||||
$this->RegisterPropertyString('IPAddress', '172.31.70.80');
|
||||
$this->RegisterPropertyInteger('LoggerNumber', 0);
|
||||
$this->RegisterPropertyInteger('PollInterval', 60); // Standard 60 Sekunden
|
||||
$this->RegisterPropertyString('Registers', '[]'); // JSON-String für Matrix
|
||||
$this->RegisterPropertyInteger('PollInterval', 60);
|
||||
$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"]);');
|
||||
}
|
||||
|
||||
@@ -21,34 +20,31 @@ class SofarWechselrichter extends IPSModule
|
||||
{
|
||||
parent::ApplyChanges();
|
||||
|
||||
// PollInterval in Millisekunden umrechnen
|
||||
// Timer-Intervall (ms) setzen
|
||||
$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);
|
||||
// Variable für vorherigen Wert (Register 1160) anlegen, falls noch nicht vorhanden
|
||||
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);
|
||||
if (!is_array($registers)) {
|
||||
$registers = [];
|
||||
}
|
||||
|
||||
// Für jeden definierten Register-Eintrag: Variable anlegen, falls nicht vorhanden
|
||||
// Für jeden Eintrag: Variable anlegen, falls nicht existiert
|
||||
$position = 20;
|
||||
foreach ($registers as $entry) {
|
||||
$regNo = (int) $entry['RegisterNumber'];
|
||||
$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 === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Existiert die Variable noch nicht? Dann anlegen
|
||||
$ident = 'Reg' . $regNo;
|
||||
if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) {
|
||||
$this->RegisterVariableFloat($ident, $label, '', $position);
|
||||
}
|
||||
@@ -56,79 +52,93 @@ 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
|
||||
{
|
||||
$ip = $this->ReadPropertyString('IPAddress');
|
||||
$loggerNumber = $this->ReadPropertyInteger('LoggerNumber');
|
||||
|
||||
// Ohne gültige Seriennummer abbrechen
|
||||
if ($loggerNumber <= 0) {
|
||||
// Ohne gültige Seriennummer bringt eine Abfrage keinen Sinn
|
||||
$this->LogMessage('LoggerNumber ist ≤ 0, Abbruch', KL_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Vorherigen Wert (Register 1160) als INT16BE auslesen
|
||||
// (1) Register 1160 als INT16BE auslesen → „Vorheriger Wert“
|
||||
try {
|
||||
$bytes1160 = $this->readRegister($loggerNumber, 1160);
|
||||
// 'n' = unsigned 16-bit big endian
|
||||
$arr1160 = unpack('nvalue', $bytes1160);
|
||||
$raw1160 = $arr1160['value'];
|
||||
// Vorzeichenkorrektur für INT16
|
||||
$bytes1160 = $this->readRegister($ip, $loggerNumber, 1160, 'BE');
|
||||
$arr1160 = unpack('nvalue', $bytes1160);
|
||||
$raw1160 = $arr1160['value'];
|
||||
// Vorzeichenkorrektur
|
||||
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);
|
||||
$this->LogMessage('Fehler Lesen Reg 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);
|
||||
if (!is_array($registers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($registers as $entry) {
|
||||
$regNo = (int) $entry['RegisterNumber'];
|
||||
$label = trim($entry['Label']);
|
||||
$scale = (float) $entry['ScalingFactor'];
|
||||
$ident = 'Reg' . $regNo;
|
||||
$regNo = (int) $entry['RegisterNumber'];
|
||||
$label = trim((string)$entry['Label']);
|
||||
$scale = (float) $entry['ScalingFactor'];
|
||||
$endian = strtoupper(trim((string)$entry['Endian']));
|
||||
$ident = 'Reg' . $regNo;
|
||||
|
||||
if ($regNo < 0 || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$bytes = $this->readRegister($loggerNumber, $regNo);
|
||||
// 'n' = unsigned 16-bit big endian
|
||||
$arr = unpack('nvalue', $bytes);
|
||||
$bytes = $this->readRegister($ip, $loggerNumber, $regNo, $endian);
|
||||
if ($endian === 'LE') {
|
||||
$arr = unpack('vvalue', $bytes); // uint16 little-endian
|
||||
} else {
|
||||
$arr = unpack('nvalue', $bytes); // uint16 big-endian
|
||||
}
|
||||
$valueRaw = $arr['value'];
|
||||
// Skaliere
|
||||
$valueScaled = $valueRaw * $scale;
|
||||
// Schreibe in die korrespondierende Float-Variable
|
||||
SetValue($this->GetIDForIdent($ident), $valueScaled);
|
||||
$value = $valueRaw * $scale;
|
||||
SetValue($this->GetIDForIdent($ident), $value);
|
||||
} 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 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
|
||||
* @param string $ip IP-Adresse des Inverters
|
||||
* @param int $serial_nr Seriennummer (Logger-Nummer)
|
||||
* @param int $reg Register-Adresse
|
||||
* @param string $endian 'BE' oder 'LE'
|
||||
* @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';
|
||||
|
||||
// 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);
|
||||
$hexSNbytes = [
|
||||
substr($hexSN, 6, 2),
|
||||
@@ -147,38 +157,19 @@ class SofarWechselrichter extends IPSModule
|
||||
$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)
|
||||
// 2) CRC16-Modbus (letzte 6 Bytes)
|
||||
$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);
|
||||
$crcValue = $this->calculateCRC16Modbus($crcInputBin);
|
||||
$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);
|
||||
$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;
|
||||
$bArr = [];
|
||||
for ($i = 0; $i < $l; $i++) {
|
||||
@@ -198,17 +189,17 @@ class SofarWechselrichter extends IPSModule
|
||||
$frameBin .= chr($b);
|
||||
}
|
||||
|
||||
// 4) TCP-Verbindung öffnen, Paket senden und Antwort einlesen
|
||||
$ip = '172.31.70.80';
|
||||
// 4) TCP-Verbindung öffnen & Paket senden (Port ist fest auf 8899)
|
||||
$port = 8899;
|
||||
$timeout = 5;
|
||||
$fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeout);
|
||||
$timeoutSec = 5;
|
||||
$fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeoutSec);
|
||||
if (!$fp) {
|
||||
throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})");
|
||||
}
|
||||
fwrite($fp, $frameBin);
|
||||
|
||||
stream_set_timeout($fp, 2);
|
||||
|
||||
// 5) Antwort einlesen
|
||||
$response = '';
|
||||
while (!feof($fp)) {
|
||||
$chunk = fread($fp, 1024);
|
||||
@@ -220,21 +211,56 @@ class SofarWechselrichter extends IPSModule
|
||||
fclose($fp);
|
||||
|
||||
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
|
||||
$numBytes = $lModbus - 4; // = 2
|
||||
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);
|
||||
if (strlen($dataBytes) < 2) {
|
||||
throw new Exception("Data-Segment < 2 Bytes.");
|
||||
throw new Exception("Data-Segment enthält weniger als 2 Bytes.");
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user