From d932bb4e744806dd7ecd607c3eee79b280a73b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=A4fliger?= Date: Wed, 4 Jun 2025 11:24:30 +0200 Subject: [PATCH] =?UTF-8?q?Sofarmodul=20fertig=20hinzugef=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SofarWechselrichter/form.json | 9 ++-- SofarWechselrichter/module.php | 91 ++++++++++++++++++++++------------ library.json | 2 +- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/SofarWechselrichter/form.json b/SofarWechselrichter/form.json index dd9bfdc..b3a46e1 100644 --- a/SofarWechselrichter/form.json +++ b/SofarWechselrichter/form.json @@ -31,7 +31,7 @@ { "caption": "Register-Nummer", "name": "RegisterNumber", - "width": "100px", + "width": "200px", "add": 0, "edit": { "type": "NumberSpinner", @@ -41,7 +41,7 @@ { "caption": "Bezeichnung", "name": "Label", - "width": "200px", + "width": "300px", "add": "", "edit": { "type": "ValidationTextBox" @@ -50,12 +50,11 @@ { "caption": "Skalierungs-faktor", "name": "ScalingFactor", - "width": "100px", + "width": "200px", "add": 1, "edit": { "type": "NumberSpinner", - "digits": 4, - "minimum": 0 + "digits": 0 } }, { diff --git a/SofarWechselrichter/module.php b/SofarWechselrichter/module.php index 717500e..944a1f9 100644 --- a/SofarWechselrichter/module.php +++ b/SofarWechselrichter/module.php @@ -2,9 +2,11 @@ declare(strict_types=1); /** - * Sofar Wechselrichter Modul + * Sofar Wechselrichter Modul (IP-Symcon) * - * Anpassung: LoggerNumber als String behandeln, da Dezimalwert > PHP-Integer + * - 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. */ class SofarWechselrichter extends IPSModule { @@ -13,11 +15,11 @@ class SofarWechselrichter extends IPSModule parent::Create(); // Moduleigenschaften $this->RegisterPropertyString('IPAddress', ''); - $this->RegisterPropertyString('LoggerNumber', '0'); // jetzt String + $this->RegisterPropertyString('LoggerNumber', '0'); // als String $this->RegisterPropertyInteger('PollInterval', 60); $this->RegisterPropertyString('Registers', '[]'); // JSON-String - // Timer für zyklische Abfragen; ruft per RequestAction ident "Query" auf + // Timer für zyklische Abfragen (per RequestAction("Query")) $script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");'; $this->RegisterTimer('QueryTimer', 0, $script); } @@ -25,16 +27,18 @@ class SofarWechselrichter extends IPSModule public function ApplyChanges() { parent::ApplyChanges(); - // Timer-Intervall (ms) setzen + // Timer-Intervall (ms) $intervalSec = $this->ReadPropertyInteger('PollInterval'); $intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0; $this->SetTimerInterval('QueryTimer', $intervalMs); - // Variablen für jeden definierten Register-Eintrag anlegen + // Aktuelle Registerliste $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers)) { $registers = []; } + + // 1) Variablen anlegen (neu hinzugekommene) $position = 10; foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; @@ -43,11 +47,31 @@ class SofarWechselrichter extends IPSModule continue; } $ident = 'Reg' . $regNo; - if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) { + if (!$this->VariableExists($ident)) { $this->RegisterVariableFloat($ident, $label, '', $position); } $position += 10; } + + // 2) Variablen löschen (falls entfernt) + // Alle Kinder-IDs durchlaufen, Variablen mit Ident "Reg" entfernen, + // wenn sie nicht mehr in $registers stehen. + $validIdents = []; + foreach ($registers as $entry) { + $validIdents[] = 'Reg' . ((int)$entry['RegisterNumber']); + } + $children = IPS_GetChildrenIDs($this->InstanceID); + foreach ($children as $childID) { + if (@IPS_VariableExists($childID)) { + $info = IPS_GetVariable($childID); + $ident = $info['VariableIdent']; + if (substr($ident, 0, 3) === 'Reg') { + if (!in_array($ident, $validIdents)) { + IPS_DeleteVariable($childID); + } + } + } + } } public function Destroy() @@ -71,35 +95,36 @@ class SofarWechselrichter extends IPSModule } /** - * Führt eine Abfrage aller in der Tabelle definierten Register durch + * Zyklische Abfrage aller definierten Register */ private function Query(): void { - $rawIP = $this->ReadPropertyString('IPAddress'); - $ip = trim($rawIP); + $ip = trim($this->ReadPropertyString('IPAddress')); $loggerNumberStr = trim($this->ReadPropertyString('LoggerNumber')); - // Validierungs-Check: IP und LoggerNumber + // 1) Validierung IP if (!filter_var($ip, FILTER_VALIDATE_IP)) { $this->LogMessage('Abbruch: Ungültige IP = "' . $ip . '"', KL_WARNING); return; } + // 2) Validierung LoggerNumber (Dezimal-String, > 0) if ($loggerNumberStr === '' || !ctype_digit($loggerNumberStr) || bccomp($loggerNumberStr, '1') < 0) { $this->LogMessage('Abbruch: Ungültige LoggerNumber = "' . $loggerNumberStr . '"', KL_WARNING); return; } - // Tabellen-Einträge holen + // 3) Register-Liste einlesen $registers = json_decode($this->ReadPropertyString('Registers'), true); if (!is_array($registers) || count($registers) === 0) { // Keine Register definiert return; } + // 4) Für jedes Register: auslesen, skalieren, in Variable schreiben foreach ($registers as $entry) { $regNo = (int) $entry['RegisterNumber']; $label = trim((string)$entry['Label']); - $scale = (float) $entry['ScalingFactor']; + $scale = (string) $entry['ScalingFactor']; // kann negativ sein $endian = strtoupper(trim((string)$entry['Endian'])); $ident = 'Reg' . $regNo; @@ -109,14 +134,16 @@ class SofarWechselrichter extends IPSModule try { $bytes = $this->readRegister($ip, $loggerNumberStr, $regNo); + // Wert als UINT16 (BE oder LE) if ($endian === 'LE') { - $arr = unpack('vvalue', $bytes); // UINT16 LE + $arr = unpack('vvalue', $bytes); // Little-Endian } else { - $arr = unpack('nvalue', $bytes); // UINT16 BE + $arr = unpack('nvalue', $bytes); // Big-Endian } $valueRaw = $arr['value']; - $value = $valueRaw * $scale; - SetValue($this->GetIDForIdent($ident), $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); } catch (Exception $e) { $this->LogMessage("Fehler Lesen Reg {$regNo}: " . $e->getMessage(), KL_WARNING); } @@ -130,7 +157,7 @@ class SofarWechselrichter extends IPSModule * @param string $serial_nr_str Logger-Seriennummer als Dezimal-String * @param int $reg Register-Adresse * @return string 2 Byte binär - * @throws Exception Bei Kommunikationsfehler + * @throws Exception Bei Kommunikationsfehlern */ private function readRegister( string $ip, @@ -141,10 +168,9 @@ class SofarWechselrichter extends IPSModule // 1) Out_Frame ohne CRC aufbauen $oFrame = 'a5170010450000'; - // Dekimal-String → 8-stellige Hex-String + // Dezimal-String → 8-stellige Hex $hexSN8 = $this->decStringToHex8($serial_nr_str); - - // Byte-Little-Endian: je 2 Hex-Zeichen umkehren + // Byte-Little-Endian: jeweils 2 Zeichen umkehren $hexSNbytes = [ substr($hexSN8, 6, 2), substr($hexSN8, 4, 2), @@ -156,12 +182,12 @@ class SofarWechselrichter extends IPSModule // Data-Field (16 Hex-Zeichen konstant) $oFrame .= '020000000000000000000000000000'; - // Business-Field: 01 03 + Start-Register (2 Bytes) + Anzahl Register (2 Bytes = 1) + // Business-Field: 01 03 + Start-Register + Anzahl Register (1) $startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT); - $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); + $numHex = str_pad(dechex(1), 4, '0', STR_PAD_LEFT); $oFrame .= '0103' . $startHex . $numHex; - // 2) CRC16-Modbus (letzte 6 Bytes) + // 2) CRC16-Modbus über letzte 6 Bytes $crcInputHex = substr($oFrame, -12); $crcInputBin = hex2bin($crcInputHex); if ($crcInputBin === false) { @@ -229,30 +255,29 @@ class SofarWechselrichter extends IPSModule } /** - * Wandelt einen Dezimal-String in einen 8-stelligen Hex-String um (ohne Präfix). + * Wandelt einen Dezimal-String in einen 8-stelligen Hex-String um. * * @param string $decString - * @return string 8-stellige Hexadezimal-Notation (Großbuchstaben) + * @return string 8-stellige Hex (uppercase) */ private function decStringToHex8(string $decString): string { - // BCMath erforderlich. Dezimal-String iterativ in Hex umwandeln. $num = ltrim($decString, '0'); if ($num === '') { return '00000000'; } $hex = ''; while (bccomp($num, '0') > 0) { - $mod = bcmod($num, '16'); - $digit = dechex((int)$mod); - $hex = strtoupper($digit) . $hex; - $num = bcdiv($num, '16', 0); + $mod = bcmod($num, '16'); + $digit = dechex((int)$mod); + $hex = strtoupper($digit) . $hex; + $num = bcdiv($num, '16', 0); } return str_pad($hex, 8, '0', STR_PAD_LEFT); } /** - * CRC16-Modbus (Init=0xFFFF, Polynom=0xA001) + * Berechnet CRC16-Modbus (Init=0xFFFF, Poly=0xA001) über Binärdaten. */ private function calculateCRC16Modbus(string $binaryData): int { @@ -273,7 +298,7 @@ class SofarWechselrichter extends IPSModule } /** - * Prüft, ob eine Variable mit Ident existiert + * Prüft, ob eine Variable mit Ident existiert. */ private function VariableExists(string $ident): bool { diff --git a/library.json b/library.json index 8670923..e7b8a9a 100644 --- a/library.json +++ b/library.json @@ -6,7 +6,7 @@ "compatibility": { "version": "8.0" }, - "version": "2.000", + "version": "2.001", "build": 0, "date": 0 } \ No newline at end of file