Files
Symcon_Belevo_Energiemanage…/SofarWechselrichter/module.php

241 lines
8.7 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()
{
// Muss immer zuerst aufgerufen werden
parent::Create();
// Eigenschaften (Properties) anlegen
$this->RegisterPropertyInteger('LoggerNumber', 0);
$this->RegisterPropertyInteger('PollInterval', 60); // Standard 60 Sekunden
$this->RegisterPropertyString('Registers', '[]'); // JSON-String für Matrix
// Timer für zyklisches Abfragen registrieren (wird in ApplyChanges aktiviert)
$this->RegisterTimer('QueryTimer', 0, 'Sofar_Query($_IPS["TARGET"]);');
}
public function ApplyChanges()
{
parent::ApplyChanges();
// PollInterval in Millisekunden umrechnen
$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);
// Aus der Property "Registers" JSON-String holen und in Array Decodieren
$registers = json_decode($this->ReadPropertyString('Registers'), true);
if (!is_array($registers)) {
$registers = [];
}
// Für jeden definierten Register-Eintrag: Variable anlegen, falls nicht vorhanden
$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
if (!IPS_VariableExists(@IPS_GetObjectIDByIdent($ident, $this->InstanceID))) {
$this->RegisterVariableFloat($ident, $label, '', $position);
}
$position += 10;
}
}
/**
* Timer-Callback: liest zyklisch alle definierten Register.
*/
public function Query(): void
{
$loggerNumber = $this->ReadPropertyInteger('LoggerNumber');
if ($loggerNumber <= 0) {
// Ohne gültige Seriennummer bringt eine Abfrage keinen Sinn
return;
}
// 1) Vorherigen Wert (Register 1160) als INT16BE auslesen
try {
$bytes1160 = $this->readRegister($loggerNumber, 1160);
// 'n' = unsigned 16-bit big endian
$arr1160 = unpack('nvalue', $bytes1160);
$raw1160 = $arr1160['value'];
// Vorzeichenkorrektur für INT16
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);
}
// 2) Tabelle auslesen und alle Einträge 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;
if ($regNo < 0 || $label === '') {
continue;
}
try {
$bytes = $this->readRegister($loggerNumber, $regNo);
// 'n' = unsigned 16-bit big endian
$arr = unpack('nvalue', $bytes);
$valueRaw = $arr['value'];
// Skaliere
$valueScaled = $valueRaw * $scale;
// Schreibe in die korrespondierende Float-Variable
SetValue($this->GetIDForIdent($ident), $valueScaled);
} catch (Exception $e) {
$this->LogMessage("Fehler beim Lesen von Register {$regNo}: " . $e->getMessage(), KL_WARNING);
}
}
}
/**
* Führt eine Modbus-ähnliche Abfrage an Sofar durch und gibt genau 2 Bytes zurück.
*
* @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
*/
private function readRegister(int $serial_nr, int $reg): string
{
// 1) Out_Frame ohne CRC erstellen
$oFrame = 'a5170010450000';
// Seriennummer in 8-stelliges Hex (jekill Byte im Little-Endian)
$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 über die letzten 6 Bytes (12 Hex-Zeichen)
$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);
$oFrameWithCRC = $oFrame . strtolower($crcSwapped);
// 3) Summen-Checksum über alle Bytes ab Index 1 + End-Byte 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 und Antwort einlesen
$ip = '172.31.70.80';
$port = 8899;
$timeout = 5;
$fp = @stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr, $timeout);
if (!$fp) {
throw new Exception("Verbindung zu {$ip}:{$port} fehlgeschlagen ({$errno}: {$errstr})");
}
fwrite($fp, $frameBin);
stream_set_timeout($fp, 2);
$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.");
}
// 5) Slice: Node-RED: l = 2*(regreg)+6 = 6. Dann slice(-l, -4) → (l4) = 2 Bytes
$lModbus = 2 * ($reg - $reg) + 6; // = 6
$numBytes = $lModbus - 4; // = 2
if (strlen($response) < $lModbus) {
throw new Exception("Unerwartet kurze Antwort (< {$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.");
}
return $dataBytes;
}
}