Lösungsansatz Abfragesteuerung Master/Slave

Hallo,

ich möchte meine Solarwechselrichter „Aurora PowerOne“ in IPS einbinden. Die Wechselrichter (2 Stück) werden über einen RS485 Bus seriell angebunden. Jeder Wechselrichter hat eine Adresse über die er sich direkt ansprechen läßt.

Der Protokollaufbau ist in etwa so:

Abfrage Werte „Adresse.Befehl.Parameter.Prüfsumme“

Antwort „Status.Wert.Wert.Wert…Prüfsumme“

Die Antwort läßt allerdings keinen Rückschluß zu welcher Wechselrichter antwortet.
Bis jetzt habe ich ein Grundgerüst, welches aus zwei Skripten besteht und einer Registervariablen besteht.
Ein Skript sendet den Befehlsfolge für die Abfrage der Werte, das zweite Skript empfängt die Werte über die Registervariable getriggert. Das funktioniert solange wie ich nur ein Wert und nur ein Wechselrichter Abfrage.

Wie kann ich jetzt am günstigsten mehrere Werte und beide Wechselrichter in einem Rutsch abfragen, oder besser wie mache ich die Zuordnung der Werte im Empfangsskript?

Kann man das auch alles mit einem Skript erledigen? Wie frage ich da mehrere Werte hintereinander ab? Oder wie warte ich auf die Antwort (Schleife, Registervariable)?

Vielleicht steh ich auch nur gerade im Wald:confused:

Ich hoffe ihr habt das Problem in etwa verstanden und könnt mir helfen!

Viele Grüße
Tommy

Hallo Tommy,
ich bin nun auch im Besitz von zwei Aurora / Power One Wechselrichter und habe diese per RS 485 verbunden. Die Kommunikation via Serial Port scheint auch grundsätzlich möglich zu sein, aber ich habe doch einige Probleme mit der PHP Umesetzung.

Folgende Hintergrundinfos habe ich zur Anbindung / dem Aurora Protokoll gefunden:
C-Files für Linux: --> Solar Powered Home – Communicating with the Aurora Inverter

und die Umsetzung von Programmiersprache C in Perl: --> Device::Inverter::Aurora - search.cpan.org

Allerdings scheitere ich bei der Umsetzung in PHP an meinen PHP & Perl Kenntnissen.

Bist du bei der Integration schon weiter gekommen und/oder könntest du deine Scripte mal posten?

Zu deiner Frage welcher Wechselrichter antwortet habe ich keine Antwort parat, aber könntest du nicht einfach die Anfrage an den 1. WR schicken und dann ein wenig warten bevor du den 2. WR ansprichst?

Besten Dank und Gruß,
Kai

Hallo,
anbei die Scripte ohne Anspruch auf Vollständigkeit und Perfektion!!
Leider habe ich im Moment keine Zeit mehr zu machen. Es funktioniert so aber ganz gut.

Skript zur Abfrage der Tagesenergie:

<?
// Aurora Power One auslesen
// Configuration parameters
// - 19200 baud (default value)
// - 1 stop bit
// - no parity

$ComID = 43290 /*[PV_Anlage]*/;

$Adress_Slave      = "\x02";
SetValue(49282 /*[Haus 52A\PV-Anlage\Adresse]*/,ord($Adress_Slave));
// Measure request to the DSP ( Voltage,Current etc.. etc.. )
//    0     1   2     3   4 5 6 7   8     9
// Address 59 Type Global - - - - CRC_L CRC_H
//             1 Grid Voltage* For three-phases systems is the mean
//             2 Grid Current* For three-phases systems is the mean
//             3 Grid Power* For three-phases systems is the mean
//             4 Frequency For three-phases systems is the mean
//             5 Vbulk For Inverter with more Bulk is the sum
$Command          = "\x4E";                        //59
SetValue(34563 /*[Haus 52A\PV-Anlage\Funktion]*/,ord($Command));
$Type             = "\x00";								//0 Tagesenergie
SetValue(53658 /*[Haus 52A\PV-Anlage\Parameter]*/,ord($Type));
$Global           = "\x00";
$Data             = "\x00\x00\x00\x00";
$FC =$Adress_Slave.$Command.$Type.$Global.$Data;  	// Datenframe
$CRC = crc_ccitt($FC);
$CRC16str = chr($CRC>>8). chr($CRC&0xff);
$Senddata = $FC.$CRC16str;
SetValue(19478 /*[Haus 52A\PV-Anlage\Sperre_Senden]*/,"1");
COMPort_SendText($ComID, $Senddata);               				// Datenframe über Comport senden

$finished = false;
$i = 0;
while ( ! $finished )
{
$Sperre = GetValue(19478 /*[Haus 52A\PV-Anlage\Sperre_Senden]*/);
  if ($Sperre == "0")
  {
  $finished = true;
  }
// Timeout
	if ( $i > 3000 )
	{
$finished = true;
	}
	$i++;
}
//
if ($Sperre == "0");
{
$Adress_Slave      = "\x03";
SetValue(49282 /*[Haus 52A\PV-Anlage\Adresse]*/,ord($Adress_Slave));
// Measure request to the DSP ( Voltage,Current etc.. etc.. )
//    0     1   2     3   4 5 6 7   8     9
// Address 59 Type Global - - - - CRC_L CRC_H
//             1 Grid Voltage* For three-phases systems is the mean
//             2 Grid Current* For three-phases systems is the mean
//             3 Grid Power* For three-phases systems is the mean
//             4 Frequency For three-phases systems is the mean
//             5 Vbulk For Inverter with more Bulk is the sum
$Command          = "\x4E";                        //59
SetValue(34563 /*[Haus 52A\PV-Anlage\Funktion]*/,ord($Command));
$Type             = "\x00";								//0 Tagesenergie
SetValue(53658 /*[Haus 52A\PV-Anlage\Parameter]*/,ord($Type));
$Global           = "\x00";
$Data             = "\x00\x00\x00\x00";
$FC =$Adress_Slave.$Command.$Type.$Global.$Data;  	// Datenframe
$CRC = crc_ccitt($FC);
$CRC16str = chr($CRC>>8). chr($CRC&0xff);
$Senddata = $FC.$CRC16str;
SetValue(19478 /*[Haus 52A\PV-Anlage\Sperre_Senden]*/,"1");
COMPort_SendText($ComID, $Senddata);
}
//
//
function crc_ccitt($str)
// CRC für Aurora Power One  Protokoll
// Thomas Unger 26.05.2011
//
//Checksum calculation
//The algorithm to compute the checksum to validate the RS485 transmission is the CRC polynomial
//standardized by CCITT:
//Bn=N^16+N^12+N^5+Bn-1
//Where N^16 means that N is elevated to the sixteenth power of 2 (i.e. it is shifted left of 16 bit)
//and where the symbol ‘+’ represents the XOR bit by bit.
//Practically, if New is the byte to process , Tmp is a swap byte and BccLo and BccHi are the low
//and high parts of the validation word, the following algorithm must be followed:
//A. Initialize BccLo=0xFF, BccHi=0xFF
//B. For each byte to transmit or receive repeat the following steps:
//1. New = New XOR BccLo
//2. Tmp=New << 4
//3. New=Tmp XOR New
//4. Tmp=New >> 5
//5. BccLo=BccHi
//6. BccHi= New XOR Tmp
//7. Tmp= New << 3
//8. BccLo= BccLo XOR Tmp
//9. Tmp= New >> 4
//10. BccLo= BccLo Xor Tmp
//C. Negate bit by bit BccLo e BccHi : CRC_L=~BccLo CRC_H=~BccHi
//
// 034E030000000000  F944 laut Internet korrekte Prüfsummen
// 024E030000000000  46C5
// 024E000000000000  3BC9
{
$BccLo = 0xFF;
$BccHi = 0xFF;
//$New=0;
for ($count=0; $count<strlen($str); $count++)
	{
	$New = ord($str[$count]) ^ $BccLo;
	$Tmp = $New << 4;
	$Tmp = $Tmp & 0xFF;
	$New = $Tmp ^ $New;
	$Tmp = $New >> 5;
	$BccLo = $BccHi;
	$BccHi= $New ^ $Tmp;
	$Tmp = $New << 3;
	$Tmp = $Tmp & 0xFF;
	$BccLo = $BccLo ^ $Tmp;
	$Tmp= $New >> 4;
	$BccLo = $BccLo ^ $Tmp;
	}
$CRC=((~$BccLo& 0xFF) << 8)|(~$BccHi& 0xFF);
return $CRC;
}
?>

Empfangsskript über Registervariable:

<?
//structure of the answer has also fixed length (6 Bytes + 2 Bytes for Checksum) :
//0                  1            2  3  4  5  6     7
//Transmission State Global State B2 B3 B4 B5 CRC_L CRC_H
//Global State:
//It shows the state of the addressed device, the details are specified in the
//description of the commands.
//Trans. State Global State Val3 Val2 Val1 Val0 CRC_L CRC_H
//The 4 bytes Val3 ... Val0 compose a float value. In order to rebuild the original float value it is
//necessary to put in sequence the 4 bytes and to read it according to the ANSI standard:
//31 30    23  22     0
//S  Exponent  Mantissa
//The value is:
//(-1)s * 2(Exponent-127) * 1.Mantissa.
// wenn das Skript von einer RegisterVariable-Instanz aus aufgerufen worden ist
// wenn das Skript von einer RegisterVariable-Instanz aus aufgerufen worden ist

if ($IPS_SENDER == "RegisterVariable")
{
  // bereits im Puffer der Instanz vorhandene Daten in $data kopieren
  $data  = RegVar_GetBuffer($IPS_INSTANCE);
  // neu empfangene Daten an $data anhängen
  $data .= $IPS_VALUE;

if (strlen($data) > 7)
  {
 $Adress_Slave = GetValue(49282 /*[Haus 52A\PV-Anlage\Adresse]*/);
 $Command = GetValue(34563 /*[Haus 52A\PV-Anlage\Funktion]*/);
 $Type = GetValue(53658 /*[Haus 52A\PV-Anlage\Parameter]*/);
//
// Status
//0 = Everything is OK.
//51 = Command is not implemented
//52 = Variable does not exist
//53 = Variable value is out of range
//54 = EEprom not accessible
//55 = Not Toggled Service Mode
//56 = Can not send the command to internal micro
//57 = Command not Executed
//58 = The variable is not available, retry
switch(ord($data[0]))
{
   case 0:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Everything is OK." );
   break;
   case 51:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Command is not implemented" );
   break;
   case 52:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Variable does not exist" );
   break;
 	case 53:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Variable value is out of range" );
   break;
   case 54:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "EEprom not accessible" );
   break;
   case 55:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Not Toggled Service Mode" );
   break;
   case 56:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "an not send the command to internal micro" );
   break;
   case 57:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "Command not Executed" );
   break;
   case 58:
	SetValue(53408 /*[Haus 52A\PV-Anlage\Übertragungsstatus]*/ , "The variable is not available, retry" );
   break;
}
  if ($Command == "78")
  {
//  $val = ord($data[5]);
 $val = ((ord($data[2])*16777216)+(ord($data[3])*65536)+(ord($data[4])*256)+ord($data[5]))/1000;

	if ($Adress_Slave =="2")
	{
	SetValue(30808 /*[Haus 52A\PV-Anlage\Daten Adresse 2\Tagesenergie]*/ , (float)$val);
	}

	if ($Adress_Slave =="3")
	{
	SetValue(33328 /*[Haus 52A\PV-Anlage\Daten Adresse 3\Tagesenergie]*/ , (float)$val);
	}
  }
  if ($Command == "59")
  {
  

  	$val = unpack("f", $data[5].$data[4].$data[3].$data[2]);
	SetValueFloat(30330 /*[Objekt #30330 existiert nicht]*/ , (float)$val[1]);
	}







      // Puffer löschen
      $data = '';
      ips_sleep(100); //Pause für neue Datenanforderung ?!
SetValue(19478 /*[Haus 52A\PV-Anlage\Sperre_Senden]*/,"0");

  }
  // Inhalt von $data im Puffer der RegisterVariable-Instanz speichern
  RegVar_SetBuffer($IPS_INSTANCE, $data);
}

function strToHex($string)
{
    $hex='';
    for ($i=0; $i < strlen($string); $i++)
    {
        $hex .= dechex(ord($string[$i]));
    }
    return $hex;
}

?>

Hallo Thomas,

vielen, vielen Dank für deine Scripte! Ich habe es tatsächlich damit hinbekommen, den Tagesertrag & Jahresertrag auszulesen.
Ich bastle im Moment noch weiter an den Scripten, ein kurzes Zwischenergebnis schon mal vorab hier.

Man braucht:
1x Serial Port I/O Instanz (19200,8,1,n)
1x Registervariable, verknüpft mit Serial Port
1x Registervariablenscript, verknüpft mit Registervariable (s.u.)
1x Wechselrichterkommandos Script (s.u.)
1x Konstantendefinitionsscript (Constants.php, angehängt)

5x Variablen:
1x string = adresse (enthält die Adresse des WR. der 1. WR hat die 2, der 2. WR die 3 usw.)
1x string = command (enthält das aufgerufene Kommando, z.B. „78“)
1x string = args (enthält ein oder mehrere Parameter des Kommandos, z.B. „3“)
1x bool = sperre_senden (enthält einen Bool Parameter, damit nicht mehrere Scripte gleichzeitig die Reg.Var vollschreiben)
1x string = transmission status (enthält das Ergebnis der Übertragungsstatus des Bus’)
1x string = var (enthält die ID der Variablen, an die das Ergebnis geschrieben werden soll)

Aufgerufen wird dann zum Beispiel die folgende Funktion:
getcumulatedenergy(52694, 2, CUMULATED_DAILY);
^^ die Funktion soll die Angabe über die vom Wechselrichter mit der ID2 über den Tag gesammelte Energie in die Symcon Variable mit der ID „52694“ schreiben.

Dazu brauchen wir also das Wechselrichter Kommando Script:

<?
include ('Constants.php');
$ComID = 26058 /*[Serial Port]*/;

function communicate ($var, $address, $command, $args) {
	$data = "";
	for ($i=0;count($args)+$i<6;$i++){
		$data = $data."\x00";
	}
	$addresshex = chr($address);
	$commandhex = chr($command);
	$c = 1;
	if (count($args)>1){
		foreach ($args as $content){
	 		$argshex[$c]= chr($content);
	 		$c++;
   	        }
	}
	if (count($args)==1){
 		$argshex= chr($args);
	}
	$FC =$addresshex.$commandhex.$argshex.$data;      // Datenframe
	$CRC = crc_ccitt($FC);
	$CRC16str = chr($CRC>>8). chr($CRC&0xff);
	$Senddata = $FC.$CRC16str;

$finished = false;
$i = 0;

while ( ! $finished )
{
   $Sperre = GetValue(16398 /*[Energie\WR	ransmission_bus\sperre_senden]*/);
   if ($Sperre == false){
   	$finished = true;
   }
   // Timeout
   if ($Sperre == true){
		 IPS_sleep(200);
   }
 	$i++;
	if ($i >= "10"){
		$finished = true;
		setvalue(16398 /*[Energie\WR	ransmission_bus\sperre_senden]*/,false);
		return "Error - ComPort in use?";
	}
}
	
$Sperre = GetValue(16398 /*[Energie\WR	ransmission_bus\sperre_senden]*/);
if ($Sperre == false);
	{
 	   $ComID = 26058 /*[Serial Port]*/;
	   setvalue(34044 /*[Energie\WR	ransmission_bus\var]*/,$var);
	   setvalue(28973 /*[Energie\WR	ransmission_bus\address]*/,$address);
	   setvalue(21813 /*[Energie\WR	ransmission_bus\command]*/,$command);
	   setvalue(49633 /*[Energie\WR	ransmission_bus\args]*/,$args);  // noch kein Array - muss noch angepasst werden, da die Parameteranzahl ggf. größer 1
      setvalue(16398 /*[Energie\WR	ransmission_bus\sperre_senden]*/,true);
		
		Comport_Sendtext($ComID, $Senddata);
	}
}

// CRC function
function crc_ccitt($str)
// CRC für Aurora Power One  Protokoll
// Thomas Unger 26.05.2011
//
//Checksum calculation
//The algorithm to compute the checksum to validate the RS485 transmission is the CRC polynomial
//standardized by CCITT:
//Bn=N^16+N^12+N^5+Bn-1
//Where N^16 means that N is elevated to the sixteenth power of 2 (i.e. it is shifted left of 16 bit)
//and where the symbol ‘+’ represents the XOR bit by bit.
//Practically, if New is the byte to process , Tmp is a swap byte and BccLo and BccHi are the low
//and high parts of the validation word, the following algorithm must be followed:
//A. Initialize BccLo=0xFF, BccHi=0xFF
//B. For each byte to transmit or receive repeat the following steps:
//1. New = New XOR BccLo
//2. Tmp=New << 4
//3. New=Tmp XOR New
//4. Tmp=New >> 5
//5. BccLo=BccHi
//6. BccHi= New XOR Tmp
//7. Tmp= New << 3
//8. BccLo= BccLo XOR Tmp
//9. Tmp= New >> 4
//10. BccLo= BccLo Xor Tmp
//C. Negate bit by bit BccLo e BccHi : CRC_L=~BccLo CRC_H=~BccHi
//
// 034E030000000000  F944 laut Internet korrekte Prüfsummen
// 024E030000000000  46C5
// 024E000000000000  3BC9
{
$BccLo = 0xFF;
$BccHi = 0xFF;
//$New=0;
for ($count=0; $count<strlen($str); $count++)
    {
    $New = ord($str[$count]) ^ $BccLo;
    $Tmp = $New << 4;
    $Tmp = $Tmp & 0xFF;
    $New = $Tmp ^ $New;
    $Tmp = $New >> 5;
    $BccLo = $BccHi;
    $BccHi= $New ^ $Tmp;
    $Tmp = $New << 3;
    $Tmp = $Tmp & 0xFF;
    $BccLo = $BccLo ^ $Tmp;
    $Tmp= $New >> 4;
    $BccLo = $BccLo ^ $Tmp;
    }
$CRC=((~$BccLo& 0xFF) << 8)|(~$BccHi& 0xFF);
return $CRC;
}

// This function is used to show the cumulated Energy in regards to the period
// period can be: 	CUMMULATED_DAILY, CUMMULATED_WEEKLY, CUMMUATED_MONTHLY,
//  						CUMMULATED_YEARLY, CUMMUALTED_TOTAL, CUMMULATED_PARTIAL;
function getCumulatedEnergy ($var, $address, $period){
	communicate($var, $address, OP_GET_CUMULATED_ENERGY, $period, 0); // OP_GET_CUMULATED_ENERGY        => 78;
}

function getDSPData ($var, $address, $parameter){
	communicate($var, $address, OP_GET_DSP, $parameter, 0); //OP_GET_DSP => 59;
}

function hextostr($hex)
{
$str='';
for ($i=0; $i < strlen($hex)-1; $i+=2)
{
$str .= chr(hexdec($hex[$i].$hex[$i+1]));
}
return $str;
}
function strToHex($string)
{
    $hex='';
    for ($i=0; $i < strlen($string); $i++)
    {
        $hex .= dechex(ord($string[$i]));
    }
    return $hex;
}
?>

Und dann noch das Registervariablenscript:

<?
//structure of the answer has also fixed length (6 Bytes + 2 Bytes for Checksum) :
//0                  1            2  3  4  5  6     7
//Transmission State Global State B2 B3 B4 B5 CRC_L CRC_H
//Global State:
//It shows the state of the addressed device, the details are specified in the
//description of the commands.
//Trans. State Global State Val3 Val2 Val1 Val0 CRC_L CRC_H
//The 4 bytes Val3 ... Val0 compose a float value. In order to rebuild the original float value it is
//necessary to put in sequence the 4 bytes and to read it according to the ANSI standard

if ($IPS_SENDER == "RegisterVariable")
{
  // bereits im Puffer der Instanz vorhandene Daten in $data kopieren
  $data  = RegVar_GetBuffer($IPS_INSTANCE);
  // neu empfangene Daten an $data anhängen
  $data .= $IPS_VALUE;
  echo "
 data: ".$data;
  echo "
 datalen ".strlen($data)."
";
  
if (strlen($data) > 7)
  {
 $Adress_Slave = GetValue(28973 /*[Energie\WR	ransmission_bus\address]*/);
 $Command = GetValue(21813 /*[Energie\WR	ransmission_bus\command]*/);
 $Type = GetValue(49633 /*[Energie\WR	ransmission_bus\args]*/);
//

// Status
//0 = Everything is OK.
//51 = Command is not implemented
//52 = Variable does not exist
//53 = Variable value is out of range
//54 = EEprom not accessible
//55 = Not Toggled Service Mode
//56 = Can not send the command to internal micro
//57 = Command not Executed
//58 = The variable is not available, retry
switch(ord($data[0]))
{
   case 0:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Everything is OK." );
   break;
   case 51:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Command is not implemented" );
   break;
   case 52:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Variable does not exist" );
   break;
     case 53:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Variable value is out of range" );
   break;
   case 54:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "EEprom not accessible" );
   break;
   case 55:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Not Toggled Service Mode" );
   break;
   case 56:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "an not send the command to internal micro" );
   break;
   case 57:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "Command not Executed" );
   break;
   case 58:
    SetValue(12255 /*[Energie\WR	ransmission_bus	ransmission_status]*/ , "The variable is not available, retry" );
   break;
}

switch($Command){
	case 78:
	 $val = ((ord($data[2])*16777216)+(ord($data[3])*65536)+(ord($data[4])*256)+ord($data[5]))/1000;
    setvalue(getvalue(34044 /*[Energie\WR	ransmission_bus\var]*/),(float)$val);
   break;
   
	case 59:
	$val  = unpack("f", $data[5].$data[4].$data[3].$data[2]);
	setvalue(getvalue(34044 /*[Energie\WR	ransmission_bus\var]*/),(float)$val[1]);
	break;
}  // end Switch ($command)
	// Puffer löschen
	$data = '';
	SetValue(16398 /*[Energie\WR	ransmission_bus\sperre_senden]*/,"0");
	setvalue(34044 /*[Energie\WR	ransmission_bus\var]*/,0);
	setvalue(28973 /*[Energie\WR	ransmission_bus\address]*/,"");
	setvalue(21813 /*[Energie\WR	ransmission_bus\command]*/,"");
	setvalue(49633 /*[Energie\WR	ransmission_bus\args]*/,"");
   ips_sleep(100); //Pause für neue Datenanforderung ?!
} // end if (strlen($data) > 7)
   // Inhalt von $data im Puffer der RegisterVariable-Instanz speichern
      RegVar_SetBuffer($IPS_INSTANCE, $data);
} // end if ($IPS_SENDER == "RegisterVariable")

function strToHex($string)
{
    $hex='';
    for ($i=0; $i < strlen($string); $i++)
    {
        $hex .= dechex(ord($string[$i]));
    }
    return $hex;
}

?>

Da ist noch einiges optimierbar und erweiterbar, und bestimmt nicht die letzte Version.
Kommentare sind von allen gerne willkommen - bin neu in der PHP Programmierung und dankbar für Feedback.

Gruß,
Kai

Constants.zip (1.25 KB)