SDongleA und modbus_tcp_lesen

Welche Geräter können mit dieser Anzeige benutzt werden?
Laderegler, Wallboxen, Batterie-Management-Systeme, WLAN Schalter mit Tasmota Firmware und Wechselrichter

Moderator: Ulrich

Kuschelmonschter
Beiträge: 56
Registriert: So 23. Jan 2022, 20:38
Hat sich bedankt: 3 Mal
Danksagung erhalten: 4 Mal

SDongleA und modbus_tcp_lesen

Beitrag von Kuschelmonschter »

Ich habe die modbus_tcp_lesen Methode ein bisschen umgeschrieben, sodass die jetzt ohne großen Timebase auskommt. Alle 100ms wird der Socket im Non-Blocking-Modus gepollt. Man erhält so zuverlässig und deutlich schneller eine Antwort (Default-Timeout=10s). Ich kann jetzt Wechselrichter+Batterie+Smart Meter in ca. 4s zuverlässig auslesen.

Code: Alles auswählen

  function modbus_tcp_lesen2($COM1, $GeraeteAdresse, $FunktionsCode, $RegisterAdresse, $RegisterAnzahl, $DatenTyp, $Retries = 3, $Timeout = 10) {
    $Daten = array();
    $DatenOK = false;
    if (strlen($RegisterAdresse) == 5) {
      $Register = dechex($RegisterAdresse);
    }
    else {
      $Register = str_pad(dechex($RegisterAdresse), 4, "0", STR_PAD_LEFT);
    }
    stream_set_blocking($COM1, false);
    $ProtocilIdentifier = "0000";
    $RegisterAnzahl = str_pad($RegisterAnzahl, 4, "0", STR_PAD_LEFT);
    $MessageLenght = str_pad(dechex(strlen($GeraeteAdresse.$FunktionsCode.$Register.$RegisterAnzahl) / 2), 4, "0", STR_PAD_LEFT);
    for ($j = 0; $j < $Retries; $j++) {
      $TransactionIdentifier = str_pad(rand(1, 199), 4, "0", STR_PAD_LEFT);
      $sendenachricht = hex2bin($TransactionIdentifier.$ProtocilIdentifier.$MessageLenght.$GeraeteAdresse.$FunktionsCode.$Register.$RegisterAnzahl);
      $Antwort = "";
      $rc = fwrite($COM1, $sendenachricht);
      $this->log_schreiben("Befehl =>  ".bin2hex($sendenachricht), "   ", 8);
      $LoopEnd = time() + $Timeout;
      do {
        $Laenge  = -1;
        // Manche Geräte senden die Daten in Häppchen.
        $Chunk = fread($COM1, 1000); // 1000 Bytes lesen
        if ($Chunk == false or strlen($Chunk) == 0) {
          usleep(100000);
          continue;
        }
    
        $Antwort .= bin2hex($Chunk);
        $this->log_schreiben("Antwort =>  ".$Antwort, "   ", 8);
        $Laenge = ((hexdec(substr($Antwort, 8, 4)) * 2) + 12); // 12 = Header Länge
        if (substr($Antwort, 0, 4) <> $TransactionIdentifier) {
          $Antwort = "";
        }
        if (substr($Antwort, 14, 2) == "83") {
          break;
        }
        if (substr($Antwort, 14, 2) == "84") {
          break;
        }
        if (substr($Antwort, 14, 2) == "82") {
          break;
        }
        if (strlen($Antwort) > $Laenge) {
          $Antwort = substr($Antwort, $Laenge);
          $this->log_schreiben("Antwort abgeschnistten =>  ".$Antwort, "   ", 8);
          break;
        }
      } while (strlen($Antwort) <> $Laenge and time() < $LoopEnd and !feof($COM1));
    
      if (strlen($Antwort) == 0) {
        $this->log_schreiben("Keine Antwort, nochmal =>  ", "   ", 7);
        continue;
      }
	
      $Daten["Register"] = $Register;
      $Daten["RawDaten"] = $Antwort;
      $Daten["Transaction"] = substr($Antwort, 0, 4);
      $Daten["Protocol"] = substr($Antwort, 4, 4);
      $Daten["Laenge"] = substr($Antwort, 8, 4);
      $Daten["Adresse"] = substr($Antwort, 12, 2);
      $Daten["Befehl"] = substr($Antwort, 14, 2);
      $Daten["DatenLaenge"] = hexdec(substr($Antwort, 16, 2));
      $Daten["Wert"] = 0;
      $Daten["KeineSonne"] = false;
      if ($Daten["Befehl"] == 83) {
        //  Die Registeradresse kann nicht ausgelesen werden.
        $DatenOK = true;
        break;
      }
      elseif ($Daten["Befehl"] == 82) {
        //  Die Registeradresse kann nicht ausgelesen werden.
        $DatenOK = true;
        break;
      }
      elseif ($Daten["Befehl"] == 84) {
        //  Die Registeradresse kann nicht ausgelesen werden.
        $DatenOK = true;
        break;
      }
      elseif ($Daten["Befehl"] == $FunktionsCode and strlen($Antwort) == $Laenge) {
        $DatenOK = true;
        break;
      }
    }
  
    stream_set_blocking($COM1, true);
  
    if ($DatenOK == true) {
      if ($Daten["Transaction"] == $TransactionIdentifier and $Daten["Adresse"] == strtolower($GeraeteAdresse) and $Daten["Befehl"] == $FunktionsCode) {
        //  Prüfen ob der Wert richtig sein kann.
        switch ($DatenTyp) {
          case "String":
            $Daten["Wert"] = $this->Hex2String(substr($Antwort, 18, (strpos($Antwort, '00', 18) - 18)));
            break;
          case "U8":
            $Daten["Wert"] = hexdec(substr($Antwort, 18, 2));
            break;
          case "U16":
            $Daten["Wert"] = hexdec(substr($Antwort, 18, $Daten["DatenLaenge"] * 2));
            break;
          case "U32":
            $Daten["Wert"] = hexdec(substr($Antwort, 18, $Daten["DatenLaenge"] * 2));
            break;
          case "U32S":
            $Daten["Wert"] = hexdec(substr($Antwort, 22, $Daten["DatenLaenge"]).substr($Antwort, 18, $Daten["DatenLaenge"]));
            break;
          case "I16":
            $Daten["Wert"] = hexdec(substr($Antwort, 18, $Daten["DatenLaenge"] * 2));
            if ($Daten["Wert"] > 32767) {
              $Daten["Wert"] = $Daten["Wert"] - 65536;
            }
            break;
          case "I32":
            $Daten["Wert"] = $this->hexdecs(substr($Antwort, 18, $Daten["DatenLaenge"] * 2));
            break;
          case "I32S":
            $Daten["Wert"] = $this->hexdecs(substr($Antwort, 22, $Daten["DatenLaenge"]).substr($Antwort, 18, $Daten["DatenLaenge"]));
            break;
          case "Float32":
            $Daten["Wert"] = round($this->hex2float32(substr($Antwort, 18, $Daten["DatenLaenge"] * 2)), 2);
            break;
          case "Hex":
            $Daten["Wert"] = substr($Antwort, 18, $Daten["DatenLaenge"] * 2);
            break;
          case "ASCII":
            $Daten["Wert"] = chr(hexdec(substr($Antwort, 18, $Daten["DatenLaenge"] * 2)));
            break;
          case "Zeichenkette":
            $Daten["Wert"] = hex2bin(substr($Antwort, 18, $Daten["DatenLaenge"] * 2));
            break;
          default:
            $Daten["Wert"] = 0;
            break;
        }
        return $Daten;
      }
      else {
        return $Daten;
      }
    }
    return false;
  }
Der Sleep nach dem Connect ist nach wie vor sinnvoll, da die erste Abfrage unmittelbar danach sonst ins Leere geht. Es funktioniert zwar auch ohne, jedoch wartet meine modbus_tcp_lesen2 10s auf eine Antwort und versucht es erst dann erneut. Mit einem Sleep von 1.2s (evtl. kann man da auch noch etwas tiefer gehen) funktioniert bereits die erste Abfrage und man verliert in weiterer Folge keine Zeit mehr.


Die Automation habe ich auch nochmal etwas aufgebohrt. Die bekommt aktuell nämlich nicht mit, wenn die Wechselrichter-Kommunikation down geht. Die Automation nimmt einfach stur den letzten Wert in der DB. Wenn man da gerade viel Überschuss hatte, bleiben die Relays permanent an. Solche Fehlerzustände erkenne ich jetzt und schalte sämtliche Relays ab (und schicke eine WhatsApp).

Damit lässt sich jetzt mit Huawei und dem SDongleA wirklich sehr gut arbeiten. Ich setze die aktuelle V200R022C10SPC108 Dongle-Firmware ein. Die Kombination läuft sehr stabil und zuverlässig. Wenn die Cloud mal größere Abfragen tätigt, kann mal die eine oder andere Abfrage per Modbus TCP in die Hose gehen. Damit kann die Automation aber mittlerweile bei mir auch umgehen und prellt bei temporären Aussetzern in der Kommunikation nicht gleich die Relays.

CemS
Beiträge: 52
Registriert: Mi 7. Dez 2022, 14:20
Hat sich bedankt: 12 Mal
Danksagung erhalten: 9 Mal

Re: SDongleA und modbus_tcp_lesen

Beitrag von CemS »

Hallo Kuschelmonschter,

ich habe die modifizierte Funktion bei mir eingebaut und die Abarbeitung hat sich auf 9s von 37s reduziert. Vielen Dank!
fuer die jenigen die es auch testen moechten
- eine sicherheitskopie von der Datei: funktionen.inc.php aus dem Verz. phpinc erstellen
- die Datei funktionen.inc.php mit dem editor oeffnen und den code Block von Kuschelmonschter einfuegen. (am besten hinter der Funktion modbus_tcp_lesen)
- eine sicherheitskopie von der Datei: huawei_LAN.php
- die Datei huawei_LAN.php mit dem editor oeffnen und die Stellen wo die Funktion "modbus_tcp_lesen" aufgerufen wird durch das Aufrufen der Funktion "modbus_tcp_lesen2" erzaetzen.


Gruss
Cem

Kuschelmonschter
Beiträge: 56
Registriert: So 23. Jan 2022, 20:38
Hat sich bedankt: 3 Mal
Danksagung erhalten: 4 Mal

Re: SDongleA und modbus_tcp_lesen

Beitrag von Kuschelmonschter »

Die huawei_LAN.php kann man auch noch optimieren. Die Sleeps zwischen den synchronen Abfragen braucht es nicht. Lediglich nach dem Connect sollte man 1.2-1.5s schlafen.

Benutzeravatar
Ulrich
Administrator
Beiträge: 5548
Registriert: Sa 7. Nov 2015, 10:33
Wohnort: Essen
Hat sich bedankt: 124 Mal
Danksagung erhalten: 835 Mal

Re: SDongleA und modbus_tcp_lesen

Beitrag von Ulrich »

Kuschelmonschter hat geschrieben:
Do 30. Nov 2023, 16:55
Die huawei_LAN.php kann man auch noch optimieren. Die Sleeps zwischen den synchronen Abfragen braucht es nicht. Lediglich nach dem Connect sollte man 800ms schlafen.
Hast du das mit verschiedenen Huawei Wechselrichtern und unterschiedlichen Dongle Softwareständen getestet? So pauschal ist die Aussage falsch.
-----------------------------------------------------
Ulrich . . . . . . . . [Projekt Administrator]

Kuschelmonschter
Beiträge: 56
Registriert: So 23. Jan 2022, 20:38
Hat sich bedankt: 3 Mal
Danksagung erhalten: 4 Mal

Re: SDongleA und modbus_tcp_lesen

Beitrag von Kuschelmonschter »

Nein. Nur mit aktuelleren Versionen. Wie geschrieben:
Ich setze die aktuelle V200R022C10SPC108 Dongle-Firmware ein. Die Kombination läuft sehr stabil und zuverlässig.

SVW2
Beiträge: 3
Registriert: So 19. Nov 2023, 23:31

Re: SDongleA und modbus_tcp_lesen

Beitrag von SVW2 »

Hallo,

vielen Dank für die Umsetzung.. ich habe das nun auch einmal bei mir Implementiert und bin gespannt auf die Wirkung :-D

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 0 Gäste