Files
Symcon_Belevo_Energiemanage…/SofarWechselrichter/module.php
2025-06-04 11:07:02 +02:00

247 lines
8.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; ruft per RequestAction ident "Query" auf
$script = 'IPS_RequestAction(' . $this->InstanceID . ', "Query", "");';
$this->RegisterTimer('QueryTimer', 0, $script);
}
public function ApplyChanges()
{
parent::ApplyChanges();
// Timer-Intervall (ms) setzen
$intervalSec = $this->ReadPropertyInteger('PollInterval');
$intervalMs = ($intervalSec > 0) ? $intervalSec * 1000 : 0;
$this->SetTimerInterval('QueryTimer', $intervalMs);
// Variablen für jeden definierten Register-Eintrag anlegen
$registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers)) {
$registers = [];
}
$position = 10;
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();
}
/**
* Wird aufgerufen durch IPS_RequestAction über Timer
*/
public function RequestAction($Ident, $Value)
{
switch ($Ident) {
case 'Query':
$this->Query();
break;
default:
throw new Exception('Ungültiger Ident: ' . $Ident);
}
}
/**
* Führt eine Abfrage aller in der Tabelle definierten Register durch
*/
private function Query(): void
{
$ip = $this->ReadPropertyString('IPAddress');
$loggerNumber = $this->ReadPropertyInteger('LoggerNumber');
if ($loggerNumber <= 0 || filter_var($ip, FILTER_VALIDATE_IP) === false) {
$this->LogMessage(
'Abbruch: LoggerNumber = ' . $loggerNumber . ', IP = "' . $ip . '"',
KL_WARNING
);
return;
}
$registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers) || count($registers) === 0) {
// Keine Register definiert
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 LE
} else {
$arr = unpack('nvalue', $bytes); // UINT16 BE
}
$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 ein einzelnes Register per Modbus-ähnlichem TCP (2 Bytes zurück)
*
* @param string $ip Inverter-IP
* @param int $serial_nr Logger-Seriennummer
* @param int $reg Register-Adresse
* @param string $endian 'BE' oder 'LE'
* @return string 2 Byte binär
* @throws Exception Bei Kommunikationsfehler
*/
private function readRegister(
string $ip,
int $serial_nr,
int $reg,
string $endian
): string
{
// 1) Out_Frame ohne CRC aufbauen
$oFrame = 'a5170010450000';
$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);
$oFrame .= '020000000000000000000000000000';
$startHex = str_pad(dechex($reg), 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)
$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));
$crcSwapped = substr($crcHex, 2, 2) . substr($crcHex, 0, 2);
$oFrameWithCRC = $oFrame . strtolower($crcSwapped);
// 3) Summen-Checksum (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 fest 8899)
$port = 8899;
$fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, 5);
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 ⇒ 2 Bytes Daten
$lModbus = 6;
$numBytes = 2;
if (strlen($response) < $lModbus) {
throw new Exception("Unerwartet kurze Antwort (< {$lModbus} Bytes).");
}
$dataBytes = substr($response, -$lModbus, $numBytes);
if (strlen($dataBytes) < 2) {
throw new Exception("Data-Segment enthält weniger als 2 Bytes.");
}
return $dataBytes;
}
/**
* CRC16-Modbus (Init=0xFFFF, Polynom=0xA001)
*/
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;
}
/**
* Prüft, ob eine Variable mit Ident existiert
*/
private function VariableExists(string $ident): bool
{
$vid = @IPS_GetObjectIDByIdent($ident, $this->InstanceID);
return ($vid !== false && IPS_VariableExists($vid));
}
}