Files
Symcon_Belevo_Energiemanage…/SofarWechselrichter/module.php
T
2025-06-04 10:49:28 +02:00

267 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
class SofarWechselrichter extends IPSModule
{
public function Create()
{
parent::Create();
// Moduleigenschaften
$this->RegisterPropertyString('IPAddress', '172.31.70.80');
$this->RegisterPropertyInteger('LoggerNumber', 0);
$this->RegisterPropertyInteger('PollInterval', 60);
$this->RegisterPropertyString('Registers', '[]'); // JSON-String
// Timer für zyklische Abfragen
$this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
// Timer-Intervall (ms) setzen
$intervalSec = $this->ReadPropertyInteger('PollInterval');
$intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0;
$this->SetTimerInterval('QueryTimer', $intervalMs);
// Variable für vorherigen Wert (Register 1160) anlegen, falls noch nicht vorhanden
if (!$this->VariableExists('PrevValue1160')) {
$this->RegisterVariableInteger('PrevValue1160', 'Vorheriger Wert (Reg 1160)', '', 10);
}
// Alle Einträge aus „Registers“-Liste aus Property lesen
$registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers)) {
$registers = [];
}
// Für jeden Eintrag: Variable anlegen, falls nicht existiert
$position = 20;
foreach ($registers as $entry) {
$regNo = (int) $entry['RegisterNumber'];
$label = trim($entry['Label']);
if ($regNo < 0 || $label === '') {
continue;
}
$ident = 'Reg' . $regNo;
if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) {
$this->RegisterVariableFloat($ident, $label, '', $position);
}
$position += 10;
}
}
public function Destroy()
{
parent::Destroy();
}
/**
* 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) {
$this->LogMessage('LoggerNumber ist ≤ 0, Abbruch', KL_WARNING);
return;
}
// (1) Register 1160 als INT16BE auslesen → „Vorheriger Wert“
try {
$bytes1160 = $this->readRegister($ip, $loggerNumber, 1160, 'BE');
$arr1160 = unpack('nvalue', $bytes1160);
$raw1160 = $arr1160['value'];
// Vorzeichenkorrektur
if ($raw1160 & 0x8000) {
$raw1160 -= 0x10000;
}
SetValue($this->GetIDForIdent('PrevValue1160'), $raw1160);
} catch (Exception $e) {
$this->LogMessage('Fehler Lesen Reg 1160: ' . $e->getMessage(), KL_WARNING);
}
// (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((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($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'];
$value = $valueRaw * $scale;
SetValue($this->GetIDForIdent($ident), $value);
} catch (Exception $e) {
$this->LogMessage("Fehler Lesen Reg {$regNo}: " . $e->getMessage(), KL_WARNING);
}
}
}
/**
* 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 $reg Register-Adresse
* @param string $endian 'BE' oder 'LE'
* @return string Binärkette mit genau 2 Bytes
* @throws Exception Bei Kommunikationsfehlern
*/
private function readRegister(
string $ip,
int $serial_nr,
int $reg,
string $endian
): string
{
// 1) Out_Frame ohne CRC aufbauen
$oFrame = 'a5170010450000';
// 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),
substr($hexSN, 4, 2),
substr($hexSN, 2, 2),
substr($hexSN, 0, 2),
];
$oFrame .= implode('', $hexSNbytes);
// Data-Field (16 Hex-Zeichen konstant)
$oFrame .= '020000000000000000000000000000';
// Business-Field: 01 03 + Start-Register (2 Bytes) + Anzahl Register (2 Bytes = 1)
$startHex = str_pad(dechex($reg), 4, '0', STR_PAD_LEFT);
$numRegs = 1;
$numHex = str_pad(dechex($numRegs), 4, '0', STR_PAD_LEFT);
$oFrame .= '0103' . $startHex . $numHex;
// 2) CRC16-Modbus (letzte 6 Bytes)
$crcInputHex = substr($oFrame, -12);
$crcInputBin = hex2bin($crcInputHex);
if ($crcInputBin === false) {
throw new Exception("Ungültiges Hex in CRC-Input: {$crcInputHex}");
}
$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 (alle Bytes ab Index 1) + 0x15
$l = strlen($oFrameWithCRC) / 2;
$bArr = [];
for ($i = 0; $i < $l; $i++) {
$byteHex = substr($oFrameWithCRC, 2 * $i, 2);
$bArr[$i] = hexdec($byteHex);
}
$crcSum = 0;
for ($i = 1; $i < $l; $i++) {
$crcSum += $bArr[$i];
$crcSum &= 0xFF;
}
$bArr[$l] = $crcSum;
$bArr[$l+1] = 0x15;
$frameBin = '';
foreach ($bArr as $b) {
$frameBin .= chr($b);
}
// 4) TCP-Verbindung öffnen & Paket senden (Port ist fest auf 8899)
$port = 8899;
$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);
if ($chunk === false || $chunk === '') {
break;
}
$response .= $chunk;
}
fclose($fp);
if ($response === '') {
throw new Exception("Keine Antwort vom Inverter erhalten.");
}
// 6) Slice-Logik: l = 2*(regreg)+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 (weniger als {$lModbus} Bytes).");
}
$dataBytes = substr($response, -$lModbus, $numBytes);
if (strlen($dataBytes) < 2) {
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));
}
}